diff --git a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx index fb59b87e..b1ed1d9f 100644 --- a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx +++ b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx @@ -96,22 +96,43 @@ const FlowCanvas: React.FC = ({ } }, [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) => { diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts index 74dafc19..b2f66863 100644 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts +++ b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts @@ -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(null); @@ -20,8 +45,8 @@ export const useWorkflowSave = () => { // 保存工作流数据 const saveWorkflow = useCallback(async (data: WorkflowSaveData): Promise => { - // 保存前验证工作流 - const validationResult = validateWorkflow(data.nodes, data.edges); + // 保存前验证(只验证业务规则,连接验证在 isValidConnection 中) + const validationResult = validateBeforeSave(data.nodes); if (!validationResult.valid) { message.error(validationResult.message || '工作流验证失败'); return false; diff --git a/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx b/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx index 0cc60827..7d582d13 100644 --- a/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx @@ -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, // 结束节点有输入 diff --git a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx index 342738ef..5a53c146 100644 --- a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx @@ -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"] diff --git a/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx b/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx index e1529839..3931df13 100644 --- a/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx @@ -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, // 开始节点无输入 diff --git a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx index dee08638..f5a88448 100644 --- a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx @@ -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 = ({ data, selected }) => { const nodeData = data as FlowNodeData; const definition = nodeData?.nodeDefinition; - // 如果没有 definition,显示错误提示 if (!definition) { - return
节点未配置定义
; + return
节点未配置
; } 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 ( -
- - {icon.content} +
+ + {config.icon.content}
); } - - // TODO: 支持自定义组件图标 - return {icon.content}; + 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 ( -
+
); }; - // 📋 渲染 Hover 菜单 - const renderHoverMenu = () => { - if (!config.features.showHoverMenu) return null; - if (!isHovered || selected) return null; - - return ( -
-
- 📋 -
-
- ); - }; - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={getContainerStyle()} - > - {/* 🔌 输入连接点 */} - {config.handles.input && ( - - )} - - {/* 📦 节点内容 */} -
- {/* 🎭 图标 */} +
+
+ {/* 输入连接点 */} + {config.handles.input && ( + + )} + + {/* 图标 */} {renderIcon()} - {/* 🏷️ 标签 */} -
+ {/* 标签 - shadcn 文本样式 */} +
{nodeData.label || definition.nodeName}
+ + {/* 输出连接点 */} + {config.handles.output && ( + + )} + + {/* 配置徽章 */} + {renderBadge()}
- - {/* 🔌 输出连接点 */} - {config.handles.output && ( - - )} - - {/* 🎖️ 配置徽章 */} - {renderBadge()} - - {/* 📋 Hover 菜单 */} - {renderHoverMenu()}
); }; export default React.memo(BaseNode); - diff --git a/frontend/src/pages/Workflow/Design/utils/validator.ts b/frontend/src/pages/Workflow/Design/utils/validator.ts deleted file mode 100644 index c69ad562..00000000 --- a/frontend/src/pages/Workflow/Design/utils/validator.ts +++ /dev/null @@ -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(); - - 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 }; -}; -