= ({ data, selected }) => {
// 椭圆形节点(开始/结束)使用特殊布局
const isEllipse = config.shape === 'ellipse';
+ // 菱形节点(网关)使用特殊布局
+ const isDiamond = config.shape === 'diamond';
+
+ // 菱形节点渲染(网关节点)
+ // ⚠️ 网关节点使用 Card 布局(与普通节点保持一致),但顶部添加菱形装饰
+ if (isDiamond) {
+ return (
+
+ {/* 输入连接点 */}
+ {config.handles.input && (
+
+ )}
+
+ {/* 使用 shadcn Card 组件(与普通节点一致) */}
+
+ {/* 顶部添加菱形装饰标识 */}
+
+
+
+ {config.icon.content}
+
+
+
+
+
+ {/* 图标 + 标题 */}
+
+ {renderIcon()}
+
+ {nodeData.label || definition.nodeName}
+
+
+
+
+ {/* 输入/输出部分 */}
+ {(renderInputSection() || renderOutputSection()) && (
+
+ {/* 输入部分 */}
+ {renderInputSection()}
+
+ {/* 输出部分 */}
+ {renderOutputSection()}
+
+ )}
+
+
+ {/* 输出连接点 */}
+ {config.handles.output && (
+
+ )}
+
+ {/* 配置徽章 */}
+ {renderBadge()}
+
+ );
+ }
return (
diff --git a/frontend/src/pages/Workflow/Design/nodes/index.ts b/frontend/src/pages/Workflow/Design/nodes/index.ts
index 23df4d49..c4dc9c41 100644
--- a/frontend/src/pages/Workflow/Design/nodes/index.ts
+++ b/frontend/src/pages/Workflow/Design/nodes/index.ts
@@ -10,6 +10,7 @@ import { EndEventNodeDefinition } from './EndEventNode';
import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode';
import { NotificationNodeDefinition } from './NotificationNode';
import { ApprovalNodeDefinition } from './ApprovalNode';
+import { GatewayNodeDefinition } from './GatewayNode';
import type { WorkflowNodeDefinition } from './types';
/**
@@ -21,6 +22,7 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
JenkinsBuildNodeDefinition,
NotificationNodeDefinition,
ApprovalNodeDefinition,
+ GatewayNodeDefinition,
];
/**
@@ -33,6 +35,7 @@ export const nodeTypes = {
JENKINS_BUILD: BaseNode,
NOTIFICATION: BaseNode,
APPROVAL: BaseNode,
+ GATEWAY_NODE: BaseNode,
};
/**
@@ -48,6 +51,7 @@ export {
JenkinsBuildNodeDefinition,
NotificationNodeDefinition,
ApprovalNodeDefinition,
+ GatewayNodeDefinition,
};
// 导出类型
diff --git a/frontend/src/pages/Workflow/Design/utils/gatewayValidation.ts b/frontend/src/pages/Workflow/Design/utils/gatewayValidation.ts
new file mode 100644
index 00000000..c40d74e4
--- /dev/null
+++ b/frontend/src/pages/Workflow/Design/utils/gatewayValidation.ts
@@ -0,0 +1,160 @@
+import type { FlowNode, FlowEdge } from '../types';
+
+/**
+ * 网关节点验证结果
+ */
+export interface GatewayValidationResult {
+ valid: boolean;
+ errors: string[];
+ warnings: string[];
+}
+
+/**
+ * 验证网关节点配置
+ * @param node 网关节点
+ * @param edges 所有边
+ * @returns 验证结果
+ */
+export const validateGatewayNode = (
+ node: FlowNode,
+ edges: FlowEdge[]
+): GatewayValidationResult => {
+ const errors: string[] = [];
+ const warnings: string[] = [];
+
+ // 只验证网关节点
+ if (node.data.nodeType !== 'GATEWAY_NODE') {
+ return { valid: true, errors: [], warnings: [] };
+ }
+
+ const gatewayType = node.data.configs?.gatewayType;
+ const nodeName = node.data.label || '网关节点';
+
+ // 获取出口连线
+ const outgoingEdges = edges.filter(e => e.source === node.id);
+
+ // 1. 检查出口连线数量
+ if (outgoingEdges.length < 2) {
+ errors.push(`${nodeName}:网关节点至少需要2条出口连线(当前${outgoingEdges.length}条)`);
+ return { valid: false, errors, warnings };
+ }
+
+ // 2. 排他网关 / 包容网关特殊验证
+ if (gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') {
+ // 检查默认分支
+ const defaultBranches = outgoingEdges.filter(e => e.data?.condition?.type === 'DEFAULT');
+ if (defaultBranches.length === 0) {
+ errors.push(`${nodeName}:${gatewayType === 'exclusiveGateway' ? '排他' : '包容'}网关必须有一个默认分支`);
+ } else if (defaultBranches.length > 1) {
+ errors.push(`${nodeName}:只能有一个默认分支(当前${defaultBranches.length}个)`);
+ }
+
+ // 检查条件分支
+ const expressionBranches = outgoingEdges.filter(e => e.data?.condition?.type === 'EXPRESSION');
+
+ if (gatewayType === 'exclusiveGateway') {
+ // 排他网关:检查优先级重复
+ const priorities = expressionBranches.map(e => e.data?.condition?.priority);
+ const uniquePriorities = new Set(priorities);
+ if (priorities.length !== uniquePriorities.size) {
+ errors.push(`${nodeName}:排他网关的分支优先级不能重复`);
+ }
+
+ // 检查是否所有条件分支都有优先级
+ const noPriorityBranches = expressionBranches.filter(e =>
+ e.data?.condition?.priority === undefined ||
+ e.data?.condition?.priority === null
+ );
+ if (noPriorityBranches.length > 0) {
+ errors.push(`${nodeName}:有${noPriorityBranches.length}条分支未设置优先级`);
+ }
+ }
+
+ // 检查条件表达式是否为空
+ const emptyExpressionBranches = expressionBranches.filter(e =>
+ !e.data?.condition?.expression ||
+ e.data?.condition?.expression.trim() === ''
+ );
+ if (emptyExpressionBranches.length > 0) {
+ warnings.push(`${nodeName}:有${emptyExpressionBranches.length}条分支的条件表达式为空`);
+ }
+ }
+
+ // 3. 并行网关验证
+ if (gatewayType === 'PARALLEL') {
+ // 并行网关不需要条件,但可以给出提示
+ const expressionBranches = outgoingEdges.filter(e => e.data?.condition?.type === 'EXPRESSION');
+ if (expressionBranches.length > 0) {
+ warnings.push(`${nodeName}:并行网关的所有分支都会执行,条件表达式将被忽略`);
+ }
+ }
+
+ // 4. 检查是否所有出口都有连接
+ const unconnectedEdges = outgoingEdges.filter(e => !e.target);
+ if (unconnectedEdges.length > 0) {
+ errors.push(`${nodeName}:有${unconnectedEdges.length}条出口连线未连接到目标节点`);
+ }
+
+ return {
+ valid: errors.length === 0,
+ errors,
+ warnings
+ };
+};
+
+/**
+ * 验证所有网关节点
+ * @param nodes 所有节点
+ * @param edges 所有边
+ * @returns 验证结果数组
+ */
+export const validateAllGateways = (
+ nodes: FlowNode[],
+ edges: FlowEdge[]
+): GatewayValidationResult[] => {
+ const gatewayNodes = nodes.filter(n => n.data.nodeType === 'GATEWAY_NODE');
+ return gatewayNodes.map(node => validateGatewayNode(node, edges));
+};
+
+/**
+ * 检查工作流中是否存在网关验证错误
+ * @param nodes 所有节点
+ * @param edges 所有边
+ * @returns 是否有错误
+ */
+export const hasGatewayErrors = (
+ nodes: FlowNode[],
+ edges: FlowEdge[]
+): boolean => {
+ const results = validateAllGateways(nodes, edges);
+ return results.some(r => !r.valid);
+};
+
+/**
+ * 获取所有网关验证错误信息
+ * @param nodes 所有节点
+ * @param edges 所有边
+ * @returns 错误信息数组
+ */
+export const getGatewayErrors = (
+ nodes: FlowNode[],
+ edges: FlowEdge[]
+): string[] => {
+ const results = validateAllGateways(nodes, edges);
+ return results.flatMap(r => r.errors);
+};
+
+/**
+ * 获取所有网关验证警告信息
+ * @param nodes 所有节点
+ * @param edges 所有边
+ * @returns 警告信息数组
+ */
+export const getGatewayWarnings = (
+ nodes: FlowNode[],
+ edges: FlowEdge[]
+): string[] => {
+ const results = validateAllGateways(nodes, edges);
+ return results.flatMap(r => r.warnings);
+};
+