1
This commit is contained in:
parent
c9b5ee9e95
commit
702c42823b
@ -96,22 +96,43 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
}
|
||||
}, [onEdgesStateChange, onEdgesChange, edges]);
|
||||
|
||||
// 连接验证
|
||||
// 连接验证 - 利用 React Flow 的实时验证功能
|
||||
const isValidConnection = useCallback((connection: Connection | Edge) => {
|
||||
// 防止自连接
|
||||
// 1. 防止自连接
|
||||
if (connection.source === connection.target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已存在连接
|
||||
// 2. 检查是否已存在连接(防止重复)
|
||||
const isDuplicate = edges.some(
|
||||
(edge) =>
|
||||
edge.source === connection.source &&
|
||||
edge.target === connection.target
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isDuplicate;
|
||||
}, [edges]);
|
||||
// 3. 获取源节点和目标节点
|
||||
const sourceNode = nodes.find(n => n.id === connection.source);
|
||||
const targetNode = nodes.find(n => n.id === connection.target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 开始节点不能作为连接目标
|
||||
if (targetNode.type === 'START_EVENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 结束节点不能作为连接源
|
||||
if (sourceNode.type === 'END_EVENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [edges, nodes]);
|
||||
|
||||
// 处理拖拽放置
|
||||
const handleDrop = useCallback((event: React.DragEvent) => {
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
|
||||
import { message } from 'antd';
|
||||
import * as definitionService from '../../Definition/service';
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import { validateWorkflow } from '../utils/validator';
|
||||
import { NodeType } from '../types';
|
||||
|
||||
interface WorkflowSaveData {
|
||||
nodes: FlowNode[];
|
||||
@ -13,6 +13,31 @@ interface WorkflowSaveData {
|
||||
definitionData?: any; // 原始工作流定义数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的保存前验证 - 只检查必需的业务规则
|
||||
* 连接层面的验证已在 FlowCanvas 的 isValidConnection 中处理
|
||||
*/
|
||||
const validateBeforeSave = (nodes: FlowNode[]): { valid: boolean; message?: string } => {
|
||||
// 1. 检查是否为空
|
||||
if (nodes.length === 0) {
|
||||
return { valid: false, message: '流程图中没有任何节点,请至少添加一个节点' };
|
||||
}
|
||||
|
||||
// 2. 检查必需的开始节点
|
||||
const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT);
|
||||
if (!hasStartNode) {
|
||||
return { valid: false, message: '流程图中必须包含开始节点' };
|
||||
}
|
||||
|
||||
// 3. 检查必需的结束节点
|
||||
const hasEndNode = nodes.some(node => node.data.nodeType === NodeType.END_EVENT);
|
||||
if (!hasEndNode) {
|
||||
return { valid: false, message: '流程图中必须包含结束节点' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
export const useWorkflowSave = () => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||
@ -20,8 +45,8 @@ export const useWorkflowSave = () => {
|
||||
|
||||
// 保存工作流数据
|
||||
const saveWorkflow = useCallback(async (data: WorkflowSaveData): Promise<boolean> => {
|
||||
// 保存前验证工作流
|
||||
const validationResult = validateWorkflow(data.nodes, data.edges);
|
||||
// 保存前验证(只验证业务规则,连接验证在 isValidConnection 中)
|
||||
const validationResult = validateBeforeSave(data.nodes);
|
||||
if (!validationResult.valid) {
|
||||
message.error(validationResult.message || '工作流验证失败');
|
||||
return false;
|
||||
|
||||
@ -13,19 +13,19 @@ export const EndEventNodeDefinition: BaseNodeDefinition = {
|
||||
|
||||
// 渲染配置(配置驱动)
|
||||
renderConfig: {
|
||||
shape: 'circle',
|
||||
size: { width: 100, height: 60 },
|
||||
shape: 'rounded-rect',
|
||||
size: { width: 80, height: 48 },
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
content: '⏹️',
|
||||
size: 40
|
||||
size: 32
|
||||
},
|
||||
theme: {
|
||||
primary: '#ef4444',
|
||||
secondary: '#f87171',
|
||||
secondary: '#dc2626',
|
||||
selectedBorder: '#3b82f6',
|
||||
hoverBorder: '#ef4444',
|
||||
gradient: ['#fef2f2', '#fee2e2']
|
||||
gradient: ['#ffffff', '#fef2f2']
|
||||
},
|
||||
handles: {
|
||||
input: true, // 结束节点有输入
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from './types';
|
||||
import {ConfigurableNodeDefinition, NodeType, NodeCategory} from './types';
|
||||
|
||||
/**
|
||||
* Jenkins构建节点定义(纯配置)
|
||||
@ -14,18 +14,18 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
// 渲染配置(配置驱动)
|
||||
renderConfig: {
|
||||
shape: 'rounded-rect',
|
||||
size: { width: 160, height: 80 },
|
||||
size: {width: 140, height: 48},
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
content: '🔨',
|
||||
size: 40
|
||||
size: 32
|
||||
},
|
||||
theme: {
|
||||
primary: '#52c41a',
|
||||
secondary: '#73d13d',
|
||||
primary: '#f59e0b',
|
||||
secondary: '#d97706',
|
||||
selectedBorder: '#3b82f6',
|
||||
hoverBorder: '#73d13d',
|
||||
gradient: ['#ffffff', '#f6ffed']
|
||||
hoverBorder: '#f59e0b',
|
||||
gradient: ['#ffffff', '#fef3c7']
|
||||
},
|
||||
handles: {
|
||||
input: true,
|
||||
@ -67,104 +67,10 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
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"]
|
||||
required: ["nodeName", "nodeCode", "jenkinsUrl"]
|
||||
},
|
||||
|
||||
// 输入映射Schema
|
||||
inputMappingSchema: {
|
||||
type: "object",
|
||||
title: "输入映射",
|
||||
description: "从上游节点接收的数据映射配置",
|
||||
properties: {
|
||||
sourceCodeUrl: {
|
||||
type: "string",
|
||||
title: "源代码仓库地址",
|
||||
description: "Git仓库的URL地址",
|
||||
default: "${upstream.gitUrl}"
|
||||
},
|
||||
buildParameters: {
|
||||
type: "object",
|
||||
title: "构建参数",
|
||||
description: "传递给Jenkins Job的构建参数",
|
||||
default: {}
|
||||
},
|
||||
environmentVariables: {
|
||||
type: "object",
|
||||
title: "环境变量",
|
||||
description: "构建时的环境变量",
|
||||
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",
|
||||
@ -195,29 +101,6 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
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: "构建中的测试结果统计",
|
||||
default: {
|
||||
totalCount: 0,
|
||||
passCount: 0,
|
||||
failCount: 0,
|
||||
skipCount: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["buildId", "buildStatus", "buildUrl"]
|
||||
|
||||
@ -13,19 +13,19 @@ export const StartEventNodeDefinition: BaseNodeDefinition = {
|
||||
|
||||
// 渲染配置(配置驱动)
|
||||
renderConfig: {
|
||||
shape: 'circle',
|
||||
size: { width: 100, height: 60 },
|
||||
shape: 'rounded-rect',
|
||||
size: { width: 80, height: 48 },
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
content: '▶️',
|
||||
size: 40
|
||||
size: 32
|
||||
},
|
||||
theme: {
|
||||
primary: '#10b981',
|
||||
secondary: '#34d399',
|
||||
secondary: '#059669',
|
||||
selectedBorder: '#3b82f6',
|
||||
hoverBorder: '#10b981',
|
||||
gradient: ['#ecfdf5', '#d1fae5']
|
||||
gradient: ['#ffffff', '#ecfdf5']
|
||||
},
|
||||
handles: {
|
||||
input: false, // 开始节点无输入
|
||||
|
||||
@ -1,244 +1,137 @@
|
||||
import React, { useState, CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { FlowNodeData } from '../../types';
|
||||
|
||||
/**
|
||||
* 通用节点组件 - 配置驱动渲染
|
||||
* 所有节点共用这一个组件,通过 renderConfig 配置不同样式
|
||||
* BaseNode - shadcn/ui 风格节点
|
||||
* 参考 React Flow 官方设计 + shadcn 设计系统
|
||||
*/
|
||||
const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
const definition = nodeData?.nodeDefinition;
|
||||
|
||||
// 如果没有 definition,显示错误提示
|
||||
if (!definition) {
|
||||
return <div>节点未配置定义</div>;
|
||||
return <div className="text-xs text-muted-foreground">节点未配置</div>;
|
||||
}
|
||||
|
||||
const config = definition.renderConfig;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// 🎨 计算容器样式
|
||||
const getContainerStyle = (): CSSProperties => {
|
||||
const { shape, size, theme } = config;
|
||||
// shadcn 风格容器类名
|
||||
const getContainerClass = () => {
|
||||
// 基础样式 - shadcn card 风格
|
||||
const baseClass = `
|
||||
relative inline-flex items-center gap-2 px-3 py-2
|
||||
bg-background border border-border
|
||||
rounded-lg shadow-sm
|
||||
transition-all duration-200
|
||||
hover:shadow-md hover:border-primary/50
|
||||
`;
|
||||
|
||||
const baseStyle: CSSProperties = {
|
||||
minWidth: `${size.width}px`,
|
||||
minHeight: `${size.height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
};
|
||||
// 选中状态 - shadcn 的 ring 效果
|
||||
const selectedClass = selected
|
||||
? 'border-primary shadow-md ring-2 ring-ring ring-offset-2 ring-offset-background'
|
||||
: '';
|
||||
|
||||
// 根据形状设置样式
|
||||
switch (shape) {
|
||||
case 'circle':
|
||||
baseStyle.borderRadius = '50%';
|
||||
baseStyle.padding = '12px';
|
||||
break;
|
||||
case 'rounded-rect':
|
||||
baseStyle.borderRadius = '12px';
|
||||
baseStyle.padding = '16px 20px';
|
||||
break;
|
||||
case 'rect':
|
||||
baseStyle.borderRadius = '4px';
|
||||
baseStyle.padding = '16px 20px';
|
||||
break;
|
||||
case 'diamond':
|
||||
baseStyle.transform = 'rotate(45deg)';
|
||||
baseStyle.padding = '16px';
|
||||
break;
|
||||
}
|
||||
|
||||
// 边框和背景
|
||||
const borderColor = selected
|
||||
? theme.selectedBorder
|
||||
: isHovered
|
||||
? theme.hoverBorder
|
||||
: '#e5e7eb';
|
||||
|
||||
baseStyle.border = `${selected ? '3px' : '2px'} solid ${borderColor}`;
|
||||
baseStyle.background = selected
|
||||
? `linear-gradient(135deg, ${theme.gradient[0]} 0%, #f0f9ff 100%)`
|
||||
: `linear-gradient(135deg, ${theme.gradient[0]} 0%, ${theme.gradient[1]} 100%)`;
|
||||
|
||||
// 阴影
|
||||
baseStyle.boxShadow = selected
|
||||
? `0 8px 16px ${theme.selectedBorder}33, 0 2px 8px ${theme.selectedBorder}22`
|
||||
: isHovered
|
||||
? `0 6px 16px ${theme.hoverBorder}26, 0 2px 6px ${theme.hoverBorder}14`
|
||||
: '0 2px 8px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)';
|
||||
|
||||
// Hover 效果
|
||||
if (isHovered) {
|
||||
baseStyle.transform = 'translateY(-2px)';
|
||||
}
|
||||
|
||||
return baseStyle;
|
||||
return `${baseClass} ${selectedClass}`;
|
||||
};
|
||||
|
||||
// 🔌 计算连接点样式
|
||||
const getHandleStyle = (): CSSProperties => {
|
||||
return {
|
||||
background: config.theme.primary,
|
||||
border: '3px solid white',
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
transition: 'all 0.2s ease',
|
||||
transform: isHovered ? 'scale(1.2)' : 'scale(1)',
|
||||
};
|
||||
};
|
||||
|
||||
// 🎭 渲染图标
|
||||
// 图标容器 - shadcn 风格
|
||||
const renderIcon = () => {
|
||||
const { icon, theme } = config;
|
||||
|
||||
if (icon.type === 'emoji') {
|
||||
if (config.icon.type === 'emoji') {
|
||||
return (
|
||||
<div style={{
|
||||
width: `${icon.size || 40}px`,
|
||||
height: `${icon.size || 40}px`,
|
||||
borderRadius: '10px',
|
||||
background: `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary} 100%)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: `0 4px 8px ${theme.primary}33`,
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
|
||||
}}>
|
||||
<span style={{ fontSize: `${(icon.size || 40) * 0.55}px` }}>
|
||||
{icon.content}
|
||||
<div
|
||||
className="
|
||||
flex items-center justify-center
|
||||
rounded-md
|
||||
transition-transform duration-200
|
||||
group-hover:scale-110
|
||||
"
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
background: `linear-gradient(135deg, ${config.theme.primary}, ${config.theme.secondary})`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-white text-base leading-none"
|
||||
>
|
||||
{config.icon.content}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: 支持自定义组件图标
|
||||
return <span>{icon.content}</span>;
|
||||
return null;
|
||||
};
|
||||
|
||||
// 🎖️ 渲染配置徽章
|
||||
// shadcn 风格连接点
|
||||
const handleClass = `
|
||||
!w-2.5 !h-2.5 !rounded-full !border-2 !border-background
|
||||
transition-all duration-200
|
||||
hover:!w-3 hover:!h-3
|
||||
`;
|
||||
|
||||
const getHandleStyle = () => ({
|
||||
background: config.theme.primary,
|
||||
});
|
||||
|
||||
// 配置徽章 - shadcn badge 风格
|
||||
const renderBadge = () => {
|
||||
if (!config.features.showBadge) return null;
|
||||
if (!nodeData.configs || Object.keys(nodeData.configs).length <= 3) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
background: `linear-gradient(135deg, ${config.theme.primary} 0%, ${config.theme.secondary} 100%)`,
|
||||
border: '2px solid white',
|
||||
fontSize: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: `0 2px 6px ${config.theme.primary}4D`,
|
||||
}}>
|
||||
<div
|
||||
className="
|
||||
absolute -top-1.5 -right-1.5
|
||||
inline-flex items-center justify-center
|
||||
w-4 h-4 rounded-full
|
||||
bg-primary text-primary-foreground
|
||||
text-[9px] font-semibold
|
||||
border-2 border-background
|
||||
shadow-sm
|
||||
"
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 📋 渲染 Hover 菜单
|
||||
const renderHoverMenu = () => {
|
||||
if (!config.features.showHoverMenu) return null;
|
||||
if (!isHovered || selected) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-12px',
|
||||
right: '8px',
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '6px',
|
||||
background: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
fontSize: '12px',
|
||||
}} title="复制">
|
||||
📋
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={getContainerStyle()}
|
||||
>
|
||||
{/* 🔌 输入连接点 */}
|
||||
{config.handles.input && (
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
style={getHandleStyle()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 📦 节点内容 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{/* 🎭 图标 */}
|
||||
<div className="group">
|
||||
<div className={getContainerClass()}>
|
||||
{/* 输入连接点 */}
|
||||
{config.handles.input && (
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className={handleClass}
|
||||
style={getHandleStyle()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 图标 */}
|
||||
{renderIcon()}
|
||||
|
||||
{/* 🏷️ 标签 */}
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
{/* 标签 - shadcn 文本样式 */}
|
||||
<div className="text-sm font-medium text-foreground whitespace-nowrap">
|
||||
{nodeData.label || definition.nodeName}
|
||||
</div>
|
||||
|
||||
{/* 输出连接点 */}
|
||||
{config.handles.output && (
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className={handleClass}
|
||||
style={getHandleStyle()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 配置徽章 */}
|
||||
{renderBadge()}
|
||||
</div>
|
||||
|
||||
{/* 🔌 输出连接点 */}
|
||||
{config.handles.output && (
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
style={getHandleStyle()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 🎖️ 配置徽章 */}
|
||||
{renderBadge()}
|
||||
|
||||
{/* 📋 Hover 菜单 */}
|
||||
{renderHoverMenu()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BaseNode);
|
||||
|
||||
|
||||
@ -1,207 +0,0 @@
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import { NodeType } from '../types';
|
||||
|
||||
/**
|
||||
* 验证结果接口
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工作流是否为空
|
||||
*/
|
||||
const validateNotEmpty = (nodes: FlowNode[]): ValidationResult => {
|
||||
if (nodes.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '流程图中没有任何节点,请至少添加一个节点'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证必需的开始和结束节点
|
||||
*/
|
||||
const validateRequiredNodes = (nodes: FlowNode[]): ValidationResult => {
|
||||
// 检查开始节点
|
||||
const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT);
|
||||
if (!hasStartNode) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '流程图中必须包含开始节点'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查结束节点
|
||||
const hasEndNode = nodes.some(node => node.data.nodeType === NodeType.END_EVENT);
|
||||
if (!hasEndNode) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '流程图中必须包含结束节点'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证节点连接完整性
|
||||
* 检查是否有孤立的节点(除了开始和结束节点可能只有单向连接)
|
||||
*/
|
||||
const validateNodeConnections = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
|
||||
if (nodes.length <= 1) {
|
||||
return { valid: true }; // 单节点或空流程不需要验证连接
|
||||
}
|
||||
|
||||
// 构建节点连接映射
|
||||
const nodeConnections = new Map<string, { incoming: number; outgoing: number }>();
|
||||
|
||||
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}"没有输出连接,请确保它连接到其他节点`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证节点配置完整性
|
||||
* 检查节点是否有必填配置
|
||||
*/
|
||||
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 = [
|
||||
() => validateNotEmpty(nodes),
|
||||
() => validateRequiredNodes(nodes),
|
||||
() => validateNodeConnections(nodes, edges),
|
||||
() => validateNodeConfigs(nodes),
|
||||
() => validateEdgeConditions(edges),
|
||||
];
|
||||
|
||||
// 依次执行验证
|
||||
for (const validator of validators) {
|
||||
const result = validator();
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user