diff --git a/backend/workflow_node_definition_fix.sql b/backend/workflow_node_definition_fix.sql deleted file mode 100644 index 6e437f62..00000000 --- a/backend/workflow_node_definition_fix.sql +++ /dev/null @@ -1,155 +0,0 @@ --- ===================================================== --- 修复 workflow_node_definition 表的初始化数据 --- ===================================================== - --- 删除注释,添加正确的节点定义数据 -INSERT INTO workflow_node_definition ( - node_type, node_code, node_name, description, category, - ui_variables, panel_variables_schema, local_variables_schema, - form_variables_schema, enabled, - create_time, create_by, update_time, update_by, version, deleted -) VALUES --- 开始节点 -( - 'START_EVENT', 'START_EVENT', '开始节点', '工作流的起点', 'EVENT', - '{ - "shape": "circle", - "size": {"width": 40, "height": 40}, - "style": { - "fill": "#e8f7ff", - "stroke": "#1890ff", - "strokeWidth": 2, - "icon": "play-circle", - "iconColor": "#1890ff" - }, - "ports": { - "groups": { - "out": { - "position": "right", - "attrs": { - "circle": {"r": 4, "fill": "#ffffff", "stroke": "#1890ff"} - } - } - } - } - }', - '{ - "type": "object", - "properties": { - "code": {"type": "string", "title": "节点Code"}, - "name": {"type": "string", "title": "节点名称"}, - "description": {"type": "string", "title": "节点描述"} - }, - "required": ["code", "name"] - }', - '{}', - '{"formItems": []}', - 1, - NOW(), 'system', NOW(), 'system', 1, 0 -), --- 结束节点 -( - 'END_EVENT', 'END_EVENT', '结束节点', '工作流的终点', 'EVENT', - '{ - "shape": "circle", - "size": {"width": 40, "height": 40}, - "style": { - "fill": "#fff1f0", - "stroke": "#ff4d4f", - "strokeWidth": 2, - "icon": "stop", - "iconColor": "#ff4d4f" - }, - "ports": { - "groups": { - "in": { - "position": "left", - "attrs": { - "circle": {"r": 4, "fill": "#ffffff", "stroke": "#1890ff"} - } - } - } - } - }', - '{ - "type": "object", - "properties": { - "code": {"type": "string", "title": "节点Code"}, - "name": {"type": "string", "title": "节点名称"}, - "description": {"type": "string", "title": "节点描述"} - }, - "required": ["code", "name"] - }', - '{}', - '{"formItems": []}', - 1, - NOW(), 'system', NOW(), 'system', 1, 0 -), --- 脚本任务节点 -( - 'SCRIPT_TASK', 'SCRIPT_TASK', '脚本任务', '脚本执行任务', 'TASK', - '{ - "shape": "rect", - "size": {"width": 120, "height": 60}, - "style": { - "fill": "#ffffff", - "stroke": "#1890ff", - "strokeWidth": 2, - "icon": "code", - "iconColor": "#1890ff" - }, - "ports": { - "groups": { - "in": { - "position": "left", - "attrs": { - "circle": {"r": 4, "fill": "#ffffff", "stroke": "#1890ff"} - } - }, - "out": { - "position": "right", - "attrs": { - "circle": {"r": 4, "fill": "#ffffff", "stroke": "#1890ff"} - } - } - } - } - }', - '{ - "type": "object", - "properties": { - "code": {"type": "string", "title": "节点Code"}, - "name": {"type": "string", "title": "节点名称"}, - "description": {"type": "string", "title": "节点描述"}, - "script": { - "type": "string", - "title": "脚本内容", - "format": "textarea" - }, - "language": { - "type": "string", - "title": "脚本语言", - "default": "shell", - "enum": ["shell", "python", "javascript"], - "enumNames": ["Shell脚本", "Python脚本", "JavaScript脚本"] - }, - "workingDirectory": { - "type": "string", - "title": "工作目录", - "default": "/tmp" - }, - "delegate": { - "type": "string", - "title": "执行委派者", - "default": "${shellTaskDelegate}" - } - }, - "required": ["code", "name", "script", "language", "delegate"] - }', - '{ - "environment": {"type": "object", "additionalProperties": {"type": "string"}} - }', - '{"formItems": []}', - 1, - NOW(), 'system', NOW(), 'system', 1, 0 -); diff --git a/frontend/.cursor/rules/project.mdc b/frontend/.cursor/rules/project.mdc index 2de7338b..658cf8a3 100644 --- a/frontend/.cursor/rules/project.mdc +++ b/frontend/.cursor/rules/project.mdc @@ -25,7 +25,7 @@ alwaysApply: true ## 上下文与设置 -你是超智能AI编程助手,集成在Windsurf IDE中(一个基于VS Code的AI增强IDE)。由于你的先进能力,你经常过于热衷于在未经明确请求的情况下实现更改,这可能导致代码逻辑破坏。为防止这种情况,你必须严格遵循本协议。 +你是超智能AI编程助手,集成在Cursor IDE中(一个基于VS Code的AI增强IDE)。由于你的先进能力,你经常过于热衷于在未经明确请求的情况下实现更改,这可能导致代码逻辑破坏。为防止这种情况,你必须严格遵循本协议。 **语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。 diff --git a/frontend/src/pages/Workflow/Design/components/NodePanel.tsx b/frontend/src/pages/Workflow/Design/components/NodePanel.tsx index 24126562..72f10ed9 100644 --- a/frontend/src/pages/Workflow/Design/components/NodePanel.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodePanel.tsx @@ -3,21 +3,6 @@ import { NodeCategory } from '../types'; import { NODE_DEFINITIONS } from '../nodes'; import type { WorkflowNodeDefinition } from '../nodes/types'; -// 图标映射函数 -const getNodeIcon = (iconName: string): string => { - const iconMap: Record = { - 'play-circle': '▶️', - 'stop-circle': '⏹️', - 'user': '👤', - 'api': '⚙️', - 'code': '📜', - 'build': '🚀', - 'jenkins': '🔨', - 'gateway': '💎' - }; - return iconMap[iconName] || '📋'; -}; - interface NodePanelProps { className?: string; } @@ -60,7 +45,7 @@ const NodePanel: React.FC = ({ className = '' }) => { }} onMouseEnter={(e) => { e.currentTarget.style.background = '#f9fafb'; - e.currentTarget.style.borderColor = nodeDefinition.uiConfig.style.fill; + e.currentTarget.style.borderColor = nodeDefinition.renderConfig.theme.primary; e.currentTarget.style.transform = 'translateX(2px)'; }} onMouseLeave={(e) => { @@ -80,9 +65,9 @@ const NodePanel: React.FC = ({ className = '' }) => { fontSize: '16px', width: '20px', textAlign: 'center', - color: nodeDefinition.uiConfig.style.fill + color: nodeDefinition.renderConfig.theme.primary }}> - {getNodeIcon(nodeDefinition.uiConfig.style.icon)} + {nodeDefinition.renderConfig.icon.content}
{ label: node.nodeName, nodeType: node.nodeType as any, category: getNodeCategory(node.nodeType) as any, - icon: nodeDefinition?.uiConfig.style.icon || getNodeIcon(node.nodeType), - color: nodeDefinition?.uiConfig.style.fill || getNodeColor(node.nodeType), + icon: nodeDefinition?.renderConfig.icon.content || getNodeIcon(node.nodeType), + color: nodeDefinition?.renderConfig.theme.primary || getNodeColor(node.nodeType), configs: node.configs || {}, inputMapping: node.inputMapping || {}, outputMapping: node.outputMapping || {}, diff --git a/frontend/src/pages/Workflow/Design/index.tsx b/frontend/src/pages/Workflow/Design/index.tsx index c9b9e7ac..6ac6c87b 100644 --- a/frontend/src/pages/Workflow/Design/index.tsx +++ b/frontend/src/pages/Workflow/Design/index.tsx @@ -358,8 +358,8 @@ const WorkflowDesignInner: React.FC = () => { label: nodeDefinition.nodeName, nodeType: nodeDefinition.nodeType, category: nodeDefinition.category, - icon: nodeDefinition.uiConfig.style.icon, - color: nodeDefinition.uiConfig.style.fill, + icon: nodeDefinition.renderConfig.icon.content, + color: nodeDefinition.renderConfig.theme.primary, // 保存原始节点定义引用,用于配置 nodeDefinition, // 初始化配置数据 diff --git a/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx b/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx index c71c404f..0cc60827 100644 --- a/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx @@ -1,10 +1,8 @@ -import React, { memo } from 'react'; -import { Handle, Position, NodeProps } from '@xyflow/react'; import { BaseNodeDefinition, NodeType, NodeCategory } from './types'; -import type { FlowNodeData } from '../types'; /** - * 结束事件节点定义(元数据) + * 结束事件节点定义(纯配置) + * 使用 BaseNode 组件渲染,无需自定义渲染代码 */ export const EndEventNodeDefinition: BaseNodeDefinition = { nodeCode: "END_EVENT", @@ -13,14 +11,29 @@ export const EndEventNodeDefinition: BaseNodeDefinition = { category: NodeCategory.EVENT, description: "工作流的结束节点", - uiConfig: { - size: { width: 80, height: 50 }, - style: { - fill: '#ff4d4f', - stroke: '#cf1322', - strokeWidth: 2, - icon: 'stop-circle', - iconColor: '#fff' + // 渲染配置(配置驱动) + renderConfig: { + shape: 'circle', + size: { width: 100, height: 60 }, + icon: { + type: 'emoji', + content: '⏹️', + size: 40 + }, + theme: { + primary: '#ef4444', + secondary: '#f87171', + selectedBorder: '#3b82f6', + hoverBorder: '#ef4444', + gradient: ['#fef2f2', '#fee2e2'] + }, + handles: { + input: true, // 结束节点有输入 + output: false // 无输出 + }, + features: { + showBadge: false, // 结束节点不显示徽章 + showHoverMenu: false // 结束节点不显示菜单 } }, @@ -52,89 +65,4 @@ export const EndEventNodeDefinition: BaseNodeDefinition = { } }; -/** - * 结束事件节点渲染组件 - */ -const EndEventNode: React.FC = ({ data, selected }) => { - const nodeData = data as FlowNodeData; - const [isHovered, setIsHovered] = React.useState(false); - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ - padding: '12px', - borderRadius: '50%', - border: `3px solid ${selected ? '#3b82f6' : '#ef4444'}`, - background: selected - ? 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)' - : 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)', - minWidth: '70px', - minHeight: '70px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - boxShadow: selected - ? '0 8px 16px rgba(59, 130, 246, 0.25), 0 2px 4px rgba(59, 130, 246, 0.15)' - : isHovered - ? '0 6px 12px rgba(239, 68, 68, 0.2), 0 2px 4px rgba(239, 68, 68, 0.1)' - : '0 2px 8px rgba(0,0,0,0.08)', - cursor: 'pointer', - transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', - transform: isHovered ? 'scale(1.05)' : 'scale(1)', - }} - > - {/* 图标 */} -
- ⏹️ -
- - {/* 输入连接点 */} - - - {/* 节点标签 */} -
- {nodeData.label || '结束'} -
-
- ); -}; - -export default memo(EndEventNode); - +// ✅ 不再需要单独的渲染组件,使用 BaseNode 即可 diff --git a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx index 7a3edcfd..342738ef 100644 --- a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx @@ -1,10 +1,8 @@ -import React, { memo } from 'react'; -import { Handle, Position, NodeProps } from '@xyflow/react'; import { ConfigurableNodeDefinition, NodeType, NodeCategory } from './types'; -import type { FlowNodeData } from '../types'; /** - * Jenkins构建节点定义(元数据) + * Jenkins构建节点定义(纯配置) + * 使用 BaseNode 组件渲染,无需自定义渲染代码 */ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { nodeCode: "JENKINS_BUILD", @@ -13,14 +11,29 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { category: NodeCategory.TASK, description: "通过Jenkins执行构建任务", - uiConfig: { + // 渲染配置(配置驱动) + renderConfig: { + shape: 'rounded-rect', size: { width: 160, height: 80 }, - style: { - fill: '#52c41a', - stroke: '#389e08', - strokeWidth: 2, - icon: 'jenkins', - iconColor: '#fff' + icon: { + type: 'emoji', + content: '🔨', + size: 40 + }, + theme: { + primary: '#52c41a', + secondary: '#73d13d', + selectedBorder: '#3b82f6', + hoverBorder: '#73d13d', + gradient: ['#ffffff', '#f6ffed'] + }, + handles: { + input: true, + output: true + }, + features: { + showBadge: true, + showHoverMenu: true } }, @@ -122,16 +135,12 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { type: "object", title: "构建参数", description: "传递给Jenkins Job的构建参数", - properties: {}, - additionalProperties: true, default: {} }, environmentVariables: { type: "object", title: "环境变量", description: "构建时的环境变量", - properties: {}, - additionalProperties: true, default: {} }, commitId: { @@ -203,12 +212,6 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { type: "object", title: "测试结果", description: "构建中的测试结果统计", - properties: { - totalCount: { type: "number", title: "总测试数", default: 0 }, - passCount: { type: "number", title: "通过数", default: 0 }, - failCount: { type: "number", title: "失败数", default: 0 }, - skipCount: { type: "number", title: "跳过数", default: 0 } - }, default: { totalCount: 0, passCount: 0, @@ -221,167 +224,4 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { } }; -/** - * Jenkins构建节点渲染组件 - */ -const JenkinsBuildNode: React.FC = ({ data, selected }) => { - const nodeData = data as FlowNodeData; - const [isHovered, setIsHovered] = React.useState(false); - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ - padding: '16px 20px', - borderRadius: '12px', - border: `2px solid ${selected ? '#3b82f6' : isHovered ? '#73d13d' : '#e5e7eb'}`, - background: selected - ? 'linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%)' - : 'linear-gradient(135deg, #ffffff 0%, #f6ffed 100%)', - minWidth: '160px', - minHeight: '80px', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - boxShadow: selected - ? '0 8px 16px rgba(59, 130, 246, 0.2), 0 2px 8px rgba(59, 130, 246, 0.1)' - : isHovered - ? '0 6px 16px rgba(82, 196, 26, 0.15), 0 2px 6px rgba(82, 196, 26, 0.08)' - : '0 2px 8px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)', - cursor: 'pointer', - transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', - transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', - }} - > - {/* 输入连接点 */} - - - {/* 节点内容 */} -
- {/* 图标背景 */} -
- 🔨 -
- - {/* 标签 */} -
- {nodeData.label || 'Jenkins构建'} -
-
- - {/* 输出连接点 */} - - - {/* 配置指示器 */} - {nodeData.configs && Object.keys(nodeData.configs).length > 3 && ( -
- ✓ -
- )} - - {/* Hover操作菜单 */} - {isHovered && !selected && ( -
-
- 📋 -
-
- )} -
- ); -}; - -export default memo(JenkinsBuildNode); - +// ✅ 不再需要单独的渲染组件,使用 BaseNode 即可 diff --git a/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx b/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx index a892cac5..e1529839 100644 --- a/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx @@ -1,10 +1,8 @@ -import React, { memo } from 'react'; -import { Handle, Position, NodeProps } from '@xyflow/react'; import { BaseNodeDefinition, NodeType, NodeCategory } from './types'; -import type { FlowNodeData } from '../types'; /** - * 开始事件节点定义(元数据) + * 开始事件节点定义(纯配置) + * 使用 BaseNode 组件渲染,无需自定义渲染代码 */ export const StartEventNodeDefinition: BaseNodeDefinition = { nodeCode: "START_EVENT", @@ -13,14 +11,29 @@ export const StartEventNodeDefinition: BaseNodeDefinition = { category: NodeCategory.EVENT, description: "工作流的起始节点", - uiConfig: { - size: { width: 80, height: 50 }, - style: { - fill: '#52c41a', - stroke: '#389e08', - strokeWidth: 2, - icon: 'play-circle', - iconColor: '#fff' + // 渲染配置(配置驱动) + renderConfig: { + shape: 'circle', + size: { width: 100, height: 60 }, + icon: { + type: 'emoji', + content: '▶️', + size: 40 + }, + theme: { + primary: '#10b981', + secondary: '#34d399', + selectedBorder: '#3b82f6', + hoverBorder: '#10b981', + gradient: ['#ecfdf5', '#d1fae5'] + }, + handles: { + input: false, // 开始节点无输入 + output: true // 有输出 + }, + features: { + showBadge: false, // 开始节点不显示徽章 + showHoverMenu: false // 开始节点不显示菜单 } }, @@ -52,89 +65,4 @@ export const StartEventNodeDefinition: BaseNodeDefinition = { } }; -/** - * 开始事件节点渲染组件 - */ -const StartEventNode: React.FC = ({ data, selected }) => { - const nodeData = data as FlowNodeData; - const [isHovered, setIsHovered] = React.useState(false); - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ - padding: '12px', - borderRadius: '50%', - border: `3px solid ${selected ? '#3b82f6' : '#10b981'}`, - background: selected - ? 'linear-gradient(135deg, #d0fce8 0%, #a7f3d0 100%)' - : 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)', - minWidth: '70px', - minHeight: '70px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - boxShadow: selected - ? '0 8px 16px rgba(59, 130, 246, 0.25), 0 2px 4px rgba(59, 130, 246, 0.15)' - : isHovered - ? '0 6px 12px rgba(16, 185, 129, 0.2), 0 2px 4px rgba(16, 185, 129, 0.1)' - : '0 2px 8px rgba(0,0,0,0.08)', - cursor: 'pointer', - transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', - transform: isHovered ? 'scale(1.05)' : 'scale(1)', - }} - > - {/* 图标 */} -
- ▶️ -
- - {/* 输出连接点 */} - - - {/* 节点标签 */} -
- {nodeData.label || '开始'} -
-
- ); -}; - -export default memo(StartEventNode); - +// ✅ 不再需要单独的渲染组件,使用 BaseNode 即可 diff --git a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx new file mode 100644 index 00000000..dee08638 --- /dev/null +++ b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx @@ -0,0 +1,244 @@ +import React, { useState, CSSProperties } from 'react'; +import { Handle, Position, NodeProps } from '@xyflow/react'; +import type { FlowNodeData } from '../../types'; + +/** + * 通用节点组件 - 配置驱动渲染 + * 所有节点共用这一个组件,通过 renderConfig 配置不同样式 + */ +const BaseNode: React.FC = ({ data, selected }) => { + const nodeData = data as FlowNodeData; + const definition = nodeData?.nodeDefinition; + + // 如果没有 definition,显示错误提示 + if (!definition) { + return
节点未配置定义
; + } + + const config = definition.renderConfig; + const [isHovered, setIsHovered] = useState(false); + + // 🎨 计算容器样式 + const getContainerStyle = (): CSSProperties => { + const { shape, size, theme } = config; + + 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)', + }; + + // 根据形状设置样式 + 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; + }; + + // 🔌 计算连接点样式 + 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)', + }; + }; + + // 🎭 渲染图标 + const renderIcon = () => { + const { icon, theme } = config; + + if (icon.type === 'emoji') { + return ( +
+ + {icon.content} + +
+ ); + } + + // TODO: 支持自定义组件图标 + return {icon.content}; + }; + + // 🎖️ 渲染配置徽章 + 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 && ( + + )} + + {/* 📦 节点内容 */} +
+ {/* 🎭 图标 */} + {renderIcon()} + + {/* 🏷️ 标签 */} +
+ {nodeData.label || definition.nodeName} +
+
+ + {/* 🔌 输出连接点 */} + {config.handles.output && ( + + )} + + {/* 🎖️ 配置徽章 */} + {renderBadge()} + + {/* 📋 Hover 菜单 */} + {renderHoverMenu()} +
+ ); +}; + +export default React.memo(BaseNode); + diff --git a/frontend/src/pages/Workflow/Design/nodes/index.ts b/frontend/src/pages/Workflow/Design/nodes/index.ts index cafc6725..62c39120 100644 --- a/frontend/src/pages/Workflow/Design/nodes/index.ts +++ b/frontend/src/pages/Workflow/Design/nodes/index.ts @@ -1,13 +1,13 @@ /** * 节点定义统一导出 - * 每个节点文件同时导出: - * 1. xxxDefinition - 节点元数据配置(给配置表单用) - * 2. default export - 节点渲染组件(给画布用) + * 所有节点使用统一的 BaseNode 组件渲染 + * 通过 renderConfig 配置不同的样式和行为 */ -import StartEventNode, { StartEventNodeDefinition } from './StartEventNode'; -import EndEventNode, { EndEventNodeDefinition } from './EndEventNode'; -import JenkinsBuildNode, { JenkinsBuildNodeDefinition } from './JenkinsBuildNode'; +import BaseNode from './components/BaseNode'; +import { StartEventNodeDefinition } from './StartEventNode'; +import { EndEventNodeDefinition } from './EndEventNode'; +import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode'; import type { WorkflowNodeDefinition } from './types'; /** @@ -21,21 +21,20 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [ /** * React Flow 节点类型映射(给 FlowCanvas 使用) + * ✨ 所有节点都使用 BaseNode 组件,通过 renderConfig 配置渲染 */ export const nodeTypes = { - START_EVENT: StartEventNode, - END_EVENT: EndEventNode, - JENKINS_BUILD: JenkinsBuildNode, + START_EVENT: BaseNode, + END_EVENT: BaseNode, + JENKINS_BUILD: BaseNode, }; /** * 单独导出(按需导入) */ export { - // 组件 - StartEventNode, - EndEventNode, - JenkinsBuildNode, + // 通用组件 + BaseNode, // 定义 StartEventNodeDefinition, diff --git a/frontend/src/pages/Workflow/Design/nodes/types.ts b/frontend/src/pages/Workflow/Design/nodes/types.ts index ae702bc5..f20b8010 100644 --- a/frontend/src/pages/Workflow/Design/nodes/types.ts +++ b/frontend/src/pages/Workflow/Design/nodes/types.ts @@ -47,30 +47,53 @@ export const getNodeCategory = (nodeType: NodeType | string): NodeCategory => { import type { ISchema } from '@formily/react'; export type JSONSchema = ISchema; -// UI 配置 +// ========== 渲染配置(配置驱动节点渲染) ========== + +// 节点尺寸 export interface NodeSize { width: number; height: number; } -export interface NodeStyle { - fill: string; - icon: string; - stroke: string; - iconColor: string; - strokeWidth: number; - iconSize?: number; - borderRadius?: string; - boxShadow?: string; - transition?: string; - fontSize?: string; - fontWeight?: string; - fontFamily?: string; +// 节点形状 +export type NodeShape = 'circle' | 'rounded-rect' | 'rect' | 'diamond'; + +// 图标配置 +export interface IconConfig { + type: 'emoji' | 'component'; // emoji 或 React 组件 + content: string; // emoji: "🔨", component: "JenkinsIcon" + size?: number; // 图标大小(默认 24) } -export interface UIConfig { - size: NodeSize; - style: NodeStyle; +// 颜色主题配置 +export interface ThemeConfig { + primary: string; // 主色 + secondary: string; // 次要色 + selectedBorder: string; // 选中边框色 + hoverBorder: string; // Hover 边框色 + gradient: [string, string]; // 背景渐变 [起始, 结束] +} + +// 连接点配置 +export interface HandlesConfig { + input: boolean; // 是否显示输入连接点 + output: boolean; // 是否显示输出连接点 +} + +// 功能开关配置 +export interface FeaturesConfig { + showBadge: boolean; // 是否显示配置徽章 + showHoverMenu: boolean; // 是否显示 Hover 菜单 +} + +// 完整的渲染配置 +export interface RenderConfig { + shape: NodeShape; // 节点形状 + size: NodeSize; // 节点尺寸 + icon: IconConfig; // 图标配置 + theme: ThemeConfig; // 颜色主题 + handles: HandlesConfig; // 连接点配置 + features: FeaturesConfig; // 功能开关 } // 基础节点定义(只有基本配置) @@ -80,8 +103,8 @@ export interface BaseNodeDefinition { nodeType: NodeType; category: NodeCategory; description: string; - uiConfig: UIConfig; - configSchema: JSONSchema; // 基本配置Schema(包含基本信息+节点配置) + renderConfig: RenderConfig; // ✨ 渲染配置(配置驱动) + configSchema: JSONSchema; // 基本配置Schema(包含基本信息+节点配置) } // 可配置节点定义(有3个TAB:基本配置、输入、输出) @@ -108,7 +131,6 @@ export interface NodeInstanceData { // UI位置信息 position: { x: number; y: number }; - uiConfig: UIConfig; } // 判断是否为可配置节点