This commit is contained in:
dengqichen 2025-10-21 14:52:38 +08:00
parent aacc7437cc
commit c9b5ee9e95
11 changed files with 383 additions and 592 deletions

View File

@ -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
);

View File

@ -25,7 +25,7 @@ alwaysApply: true
## 上下文与设置 ## 上下文与设置
<a id="上下文与设置"></a> <a id="上下文与设置"></a>
你是超智能AI编程助手集成在Windsurf IDE中一个基于VS Code的AI增强IDE。由于你的先进能力你经常过于热衷于在未经明确请求的情况下实现更改这可能导致代码逻辑破坏。为防止这种情况你必须严格遵循本协议。 你是超智能AI编程助手集成在Cursor IDE中一个基于VS Code的AI增强IDE。由于你的先进能力你经常过于热衷于在未经明确请求的情况下实现更改这可能导致代码逻辑破坏。为防止这种情况你必须严格遵循本协议。
**语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。 **语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。

View File

@ -3,21 +3,6 @@ import { NodeCategory } from '../types';
import { NODE_DEFINITIONS } from '../nodes'; import { NODE_DEFINITIONS } from '../nodes';
import type { WorkflowNodeDefinition } from '../nodes/types'; import type { WorkflowNodeDefinition } from '../nodes/types';
// 图标映射函数
const getNodeIcon = (iconName: string): string => {
const iconMap: Record<string, string> = {
'play-circle': '▶️',
'stop-circle': '⏹️',
'user': '👤',
'api': '⚙️',
'code': '📜',
'build': '🚀',
'jenkins': '🔨',
'gateway': '💎'
};
return iconMap[iconName] || '📋';
};
interface NodePanelProps { interface NodePanelProps {
className?: string; className?: string;
} }
@ -60,7 +45,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#f9fafb'; 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)'; e.currentTarget.style.transform = 'translateX(2px)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
@ -80,9 +65,9 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
fontSize: '16px', fontSize: '16px',
width: '20px', width: '20px',
textAlign: 'center', textAlign: 'center',
color: nodeDefinition.uiConfig.style.fill color: nodeDefinition.renderConfig.theme.primary
}}> }}>
{getNodeIcon(nodeDefinition.uiConfig.style.icon)} {nodeDefinition.renderConfig.icon.content}
</div> </div>
<div> <div>
<div style={{ <div style={{

View File

@ -45,8 +45,8 @@ export const useWorkflowLoad = () => {
label: node.nodeName, label: node.nodeName,
nodeType: node.nodeType as any, nodeType: node.nodeType as any,
category: getNodeCategory(node.nodeType) as any, category: getNodeCategory(node.nodeType) as any,
icon: nodeDefinition?.uiConfig.style.icon || getNodeIcon(node.nodeType), icon: nodeDefinition?.renderConfig.icon.content || getNodeIcon(node.nodeType),
color: nodeDefinition?.uiConfig.style.fill || getNodeColor(node.nodeType), color: nodeDefinition?.renderConfig.theme.primary || getNodeColor(node.nodeType),
configs: node.configs || {}, configs: node.configs || {},
inputMapping: node.inputMapping || {}, inputMapping: node.inputMapping || {},
outputMapping: node.outputMapping || {}, outputMapping: node.outputMapping || {},

View File

@ -358,8 +358,8 @@ const WorkflowDesignInner: React.FC = () => {
label: nodeDefinition.nodeName, label: nodeDefinition.nodeName,
nodeType: nodeDefinition.nodeType, nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category, category: nodeDefinition.category,
icon: nodeDefinition.uiConfig.style.icon, icon: nodeDefinition.renderConfig.icon.content,
color: nodeDefinition.uiConfig.style.fill, color: nodeDefinition.renderConfig.theme.primary,
// 保存原始节点定义引用,用于配置 // 保存原始节点定义引用,用于配置
nodeDefinition, nodeDefinition,
// 初始化配置数据 // 初始化配置数据

View File

@ -1,10 +1,8 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import { BaseNodeDefinition, NodeType, NodeCategory } from './types'; import { BaseNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/** /**
* *
* 使 BaseNode
*/ */
export const EndEventNodeDefinition: BaseNodeDefinition = { export const EndEventNodeDefinition: BaseNodeDefinition = {
nodeCode: "END_EVENT", nodeCode: "END_EVENT",
@ -13,14 +11,29 @@ export const EndEventNodeDefinition: BaseNodeDefinition = {
category: NodeCategory.EVENT, category: NodeCategory.EVENT,
description: "工作流的结束节点", description: "工作流的结束节点",
uiConfig: { // 渲染配置(配置驱动)
size: { width: 80, height: 50 }, renderConfig: {
style: { shape: 'circle',
fill: '#ff4d4f', size: { width: 100, height: 60 },
stroke: '#cf1322', icon: {
strokeWidth: 2, type: 'emoji',
icon: 'stop-circle', content: '⏹️',
iconColor: '#fff' 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 = {
} }
}; };
/** // ✅ 不再需要单独的渲染组件,使用 BaseNode 即可
*
*/
const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
const [isHovered, setIsHovered] = React.useState(false);
return (
<div
onMouseEnter={() => 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)',
}}
>
{/* 图标 */}
<div style={{
fontSize: '26px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
filter: selected ? 'brightness(1.1)' : 'none',
transition: 'filter 0.2s ease'
}}>
</div>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Left}
style={{
background: '#ef4444',
border: '3px solid white',
width: '12px',
height: '12px',
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
transition: 'all 0.2s ease',
}}
/>
{/* 节点标签 */}
<div
style={{
position: 'absolute',
bottom: '-30px',
left: '50%',
transform: 'translateX(-50%)',
fontSize: '13px',
color: '#1f2937',
fontWeight: '600',
whiteSpace: 'nowrap',
background: 'white',
padding: '4px 10px',
borderRadius: '6px',
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
border: '1px solid #e5e7eb',
}}
>
{nodeData.label || '结束'}
</div>
</div>
);
};
export default memo(EndEventNode);

View File

@ -1,10 +1,8 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from './types'; import { ConfigurableNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/** /**
* Jenkins构建节点定义 * Jenkins构建节点定义
* 使 BaseNode
*/ */
export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
nodeCode: "JENKINS_BUILD", nodeCode: "JENKINS_BUILD",
@ -13,14 +11,29 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
category: NodeCategory.TASK, category: NodeCategory.TASK,
description: "通过Jenkins执行构建任务", description: "通过Jenkins执行构建任务",
uiConfig: { // 渲染配置(配置驱动)
renderConfig: {
shape: 'rounded-rect',
size: { width: 160, height: 80 }, size: { width: 160, height: 80 },
style: { icon: {
fill: '#52c41a', type: 'emoji',
stroke: '#389e08', content: '🔨',
strokeWidth: 2, size: 40
icon: 'jenkins', },
iconColor: '#fff' 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", type: "object",
title: "构建参数", title: "构建参数",
description: "传递给Jenkins Job的构建参数", description: "传递给Jenkins Job的构建参数",
properties: {},
additionalProperties: true,
default: {} default: {}
}, },
environmentVariables: { environmentVariables: {
type: "object", type: "object",
title: "环境变量", title: "环境变量",
description: "构建时的环境变量", description: "构建时的环境变量",
properties: {},
additionalProperties: true,
default: {} default: {}
}, },
commitId: { commitId: {
@ -203,12 +212,6 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
type: "object", type: "object",
title: "测试结果", title: "测试结果",
description: "构建中的测试结果统计", 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: { default: {
totalCount: 0, totalCount: 0,
passCount: 0, passCount: 0,
@ -221,167 +224,4 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
} }
}; };
/** // ✅ 不再需要单独的渲染组件,使用 BaseNode 即可
* Jenkins构建节点渲染组件
*/
const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
const [isHovered, setIsHovered] = React.useState(false);
return (
<div
onMouseEnter={() => 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)',
}}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Left}
style={{
background: '#52c41a',
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)',
}}
/>
{/* 节点内容 */}
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '8px'
}}>
{/* 图标背景 */}
<div style={{
width: '40px',
height: '40px',
borderRadius: '10px',
background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 8px rgba(82, 196, 26, 0.2)',
transition: 'transform 0.2s ease',
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
}}>
<span style={{ fontSize: '22px' }}>🔨</span>
</div>
{/* 标签 */}
<div style={{
fontSize: '14px',
color: '#1f2937',
fontWeight: '600',
textAlign: 'center',
lineHeight: '1.2'
}}>
{nodeData.label || 'Jenkins构建'}
</div>
</div>
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Right}
style={{
background: '#52c41a',
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)',
}}
/>
{/* 配置指示器 */}
{nodeData.configs && Object.keys(nodeData.configs).length > 3 && (
<div
style={{
position: 'absolute',
top: '-8px',
right: '-8px',
width: '20px',
height: '20px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)',
border: '2px solid white',
fontSize: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 'bold',
boxShadow: '0 2px 6px rgba(16, 185, 129, 0.3)',
}}
>
</div>
)}
{/* Hover操作菜单 */}
{isHovered && !selected && (
<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>
)}
</div>
);
};
export default memo(JenkinsBuildNode);

View File

@ -1,10 +1,8 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import { BaseNodeDefinition, NodeType, NodeCategory } from './types'; import { BaseNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/** /**
* *
* 使 BaseNode
*/ */
export const StartEventNodeDefinition: BaseNodeDefinition = { export const StartEventNodeDefinition: BaseNodeDefinition = {
nodeCode: "START_EVENT", nodeCode: "START_EVENT",
@ -13,14 +11,29 @@ export const StartEventNodeDefinition: BaseNodeDefinition = {
category: NodeCategory.EVENT, category: NodeCategory.EVENT,
description: "工作流的起始节点", description: "工作流的起始节点",
uiConfig: { // 渲染配置(配置驱动)
size: { width: 80, height: 50 }, renderConfig: {
style: { shape: 'circle',
fill: '#52c41a', size: { width: 100, height: 60 },
stroke: '#389e08', icon: {
strokeWidth: 2, type: 'emoji',
icon: 'play-circle', content: '▶️',
iconColor: '#fff' 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 = {
} }
}; };
/** // ✅ 不再需要单独的渲染组件,使用 BaseNode 即可
*
*/
const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
const [isHovered, setIsHovered] = React.useState(false);
return (
<div
onMouseEnter={() => 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)',
}}
>
{/* 图标 */}
<div style={{
fontSize: '26px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
filter: selected ? 'brightness(1.1)' : 'none',
transition: 'filter 0.2s ease'
}}>
</div>
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Right}
style={{
background: '#10b981',
border: '3px solid white',
width: '12px',
height: '12px',
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
transition: 'all 0.2s ease',
}}
/>
{/* 节点标签 */}
<div
style={{
position: 'absolute',
bottom: '-30px',
left: '50%',
transform: 'translateX(-50%)',
fontSize: '13px',
color: '#1f2937',
fontWeight: '600',
whiteSpace: 'nowrap',
background: 'white',
padding: '4px 10px',
borderRadius: '6px',
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
border: '1px solid #e5e7eb',
}}
>
{nodeData.label || '开始'}
</div>
</div>
);
};
export default memo(StartEventNode);

View File

@ -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<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
const definition = nodeData?.nodeDefinition;
// 如果没有 definition显示错误提示
if (!definition) {
return <div></div>;
}
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 (
<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}
</span>
</div>
);
}
// TODO: 支持自定义组件图标
return <span>{icon.content}</span>;
};
// 🎖️ 渲染配置徽章
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>
);
};
// 📋 渲染 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'
}}>
{/* 🎭 图标 */}
{renderIcon()}
{/* 🏷️ 标签 */}
<div style={{
fontSize: '14px',
color: '#1f2937',
fontWeight: '600',
textAlign: 'center',
lineHeight: '1.2'
}}>
{nodeData.label || definition.nodeName}
</div>
</div>
{/* 🔌 输出连接点 */}
{config.handles.output && (
<Handle
type="source"
position={Position.Right}
style={getHandleStyle()}
/>
)}
{/* 🎖️ 配置徽章 */}
{renderBadge()}
{/* 📋 Hover 菜单 */}
{renderHoverMenu()}
</div>
);
};
export default React.memo(BaseNode);

View File

@ -1,13 +1,13 @@
/** /**
* *
* * 使 BaseNode
* 1. xxxDefinition - * renderConfig
* 2. default export -
*/ */
import StartEventNode, { StartEventNodeDefinition } from './StartEventNode'; import BaseNode from './components/BaseNode';
import EndEventNode, { EndEventNodeDefinition } from './EndEventNode'; import { StartEventNodeDefinition } from './StartEventNode';
import JenkinsBuildNode, { JenkinsBuildNodeDefinition } from './JenkinsBuildNode'; import { EndEventNodeDefinition } from './EndEventNode';
import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode';
import type { WorkflowNodeDefinition } from './types'; import type { WorkflowNodeDefinition } from './types';
/** /**
@ -21,21 +21,20 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
/** /**
* React Flow FlowCanvas 使 * React Flow FlowCanvas 使
* 使 BaseNode renderConfig
*/ */
export const nodeTypes = { export const nodeTypes = {
START_EVENT: StartEventNode, START_EVENT: BaseNode,
END_EVENT: EndEventNode, END_EVENT: BaseNode,
JENKINS_BUILD: JenkinsBuildNode, JENKINS_BUILD: BaseNode,
}; };
/** /**
* *
*/ */
export { export {
// 组件 // 通用组件
StartEventNode, BaseNode,
EndEventNode,
JenkinsBuildNode,
// 定义 // 定义
StartEventNodeDefinition, StartEventNodeDefinition,

View File

@ -47,30 +47,53 @@ export const getNodeCategory = (nodeType: NodeType | string): NodeCategory => {
import type { ISchema } from '@formily/react'; import type { ISchema } from '@formily/react';
export type JSONSchema = ISchema; export type JSONSchema = ISchema;
// UI 配置 // ========== 渲染配置(配置驱动节点渲染) ==========
// 节点尺寸
export interface NodeSize { export interface NodeSize {
width: number; width: number;
height: number; height: number;
} }
export interface NodeStyle { // 节点形状
fill: string; export type NodeShape = 'circle' | 'rounded-rect' | 'rect' | 'diamond';
icon: string;
stroke: string; // 图标配置
iconColor: string; export interface IconConfig {
strokeWidth: number; type: 'emoji' | 'component'; // emoji 或 React 组件
iconSize?: number; content: string; // emoji: "🔨", component: "JenkinsIcon"
borderRadius?: string; size?: number; // 图标大小(默认 24
boxShadow?: string;
transition?: string;
fontSize?: string;
fontWeight?: string;
fontFamily?: string;
} }
export interface UIConfig { // 颜色主题配置
size: NodeSize; export interface ThemeConfig {
style: NodeStyle; 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; nodeType: NodeType;
category: NodeCategory; category: NodeCategory;
description: string; description: string;
uiConfig: UIConfig; renderConfig: RenderConfig; // ✨ 渲染配置(配置驱动)
configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置) configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置)
} }
// 可配置节点定义有3个TAB基本配置、输入、输出 // 可配置节点定义有3个TAB基本配置、输入、输出
@ -108,7 +131,6 @@ export interface NodeInstanceData {
// UI位置信息 // UI位置信息
position: { x: number; y: number }; position: { x: number; y: number };
uiConfig: UIConfig;
} }
// 判断是否为可配置节点 // 判断是否为可配置节点