feat(flow): 添加代码节点编辑功能

- 新增 CodeEditor 组件支持代码编辑与参数配置
- 实现 CodeMirror 组件用于代码高亮与语言切换
- 添加代码节点默认模板与初始化逻辑
- 扩展节点类型支持 CODE 类型并注册对应组件
- 更新本地节点数据配置以支持代码节点参数定义
- 增加节点内容展示组件 nodeContentCode 用于显示代码节点信息
- 优化事件节点内容解析逻辑,增强 JSON 数据判断
master
钟良源 4 months ago
parent 19f7d0cc0d
commit d8f80e62ea

@ -0,0 +1,30 @@
import React from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import NodeContentCode from '@/pages/flowEditor/components/nodeContentCode';
const setIcon = () => {
return <DynamicIcon type="IconCode" style={{ fontSize: '16px', marginRight: '5px' }} />;
};
const CodeNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '代码编辑器';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon()}
{title}
</div>
<NodeContentCode data={data} />
</div>
);
};
export default CodeNode;

@ -1,10 +1,46 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors'; import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react'; import { Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon'; import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable'; import ParamsTable from './ParamsTable';
import CodeMirror from '@/components/FlowEditor/nodeEditors/components/CodeMirror';
const CodeEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => { const CodeEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
const [codeComponent, setCodeComponent] = useState(nodeData.component || {});
// 当组件加载时,主动触发 CodeMirror 的 onUpdateData 方法
useEffect(() => {
// 如果是新创建的节点,没有默认的 component 数据,则设置默认值
if (!nodeData.component || Object.keys(nodeData.component).length === 0) {
const defaultData = {
customDef: {
languageId: '63', // 默认 Java
sourceCode: '/**\n' +
'ExecClass类main 方法是固定的启动函数,参数个数、类型、返回类型不可更改\n' +
'当前版本gson-2.10.1.jar的文档地址\n' +
'https://www.javadoc.io/doc/com.google.code.gson/gson/2.10.1/index.html\n' +
'*/\n' +
'import com.google.gson.JsonObject;\n' +
'class ExecClass{ \n' +
' public JsonObject main(JsonObject args){\n' +
' return args;\n' +
' }\n' +
'}'
},
type: 'CODE'
};
// 更新节点数据
updateNodeData('component', defaultData);
setCodeComponent(defaultData);
}
else {
// 如果已有 component 数据,则使用现有数据
setCodeComponent(nodeData.component);
}
}, []);
return ( return (
<> <>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title> <Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
@ -17,6 +53,26 @@ const CodeEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) =>
}); });
}} }}
/> />
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<CodeMirror
initialData={codeComponent}
onUpdateData={(data) => {
setCodeComponent(data);
updateNodeData('component', {
...data
});
}}
/>
</> </>
); );
}; };

@ -0,0 +1,98 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Select } from '@arco-design/web-react';
import CodeMirror from '@uiw/react-codemirror';
import { java } from '@codemirror/lang-java';
import { python } from '@codemirror/lang-python';
import { githubLight } from '@uiw/codemirror-theme-github';
const Option = Select.Option;
interface TableDataItem {
key: number | string;
id: string;
dataType: string;
arrayType: string;
desc: string;
defaultValue: string;
[key: string]: any; // 允许其他自定义字段
}
interface CodeMirrorProps {
initialData: TableDataItem[],
onUpdateData: (data) => void,
}
const extensions = [java(), python(), githubLight];
const options = ['java', 'python'];
const defaultCode = {
'java': '/**\n' +
'ExecClass类main 方法是固定的启动函数,参数个数、类型、返回类型不可更改\n' +
'当前版本gson-2.10.1.jar的文档地址\n' +
'https://www.javadoc.io/doc/com.google.code.gson/gson/2.10.1/index.html\n' +
'*/\n' +
'import com.google.gson.JsonObject;\n' +
'class ExecClass{ \n' +
' public JsonObject main(JsonObject args){\n' +
' return args;\n' +
' }\n' +
'}',
'python': '# main函数是启动函数参数类型、个数、返回类型不可更改\n' +
'def main(a:dict)->dict:\n' +
' return {\'b\': a.get(\'a\')+[4,5]}'
};
const nameToCode = {
'java': '63',
'python': '70'
};
const CodeMirrorComp: React.FC<CodeMirrorProps> = ({
initialData,
onUpdateData
}) => {
const [value, setValue] = useState('console.log(\'hello world!\');');
const [language, setLanguage] = useState('java');
const onChange = useCallback((val, viewUpdate) => {
setValue(val);
const data = {
customDef: {
languageId: nameToCode[language],
sourceCode: val
}
};
console.log('data:', data);
onUpdateData(data);
}, []);
useEffect(() => {
setValue(defaultCode[language]);
}, []);
return (
<>
<Select
defaultValue={'java'}
placeholder="请选择语言"
style={{ width: 154 }}
onChange={(value) => {
setLanguage(value);
setValue(defaultCode[value]);
}}
>
{options.map((option, index) => (
<Option key={option} disabled={index === 3} value={option}>
{option}
</Option>
))}
</Select>
<CodeMirror
value={value}
height="300px"
extensions={extensions}
onChange={onChange} />
</>
);
};
export default CodeMirrorComp;

@ -21,6 +21,7 @@ import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode'; import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode'; import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode'; import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import { updateCanvasDataMap } from '@/store/ideContainer'; import { updateCanvasDataMap } from '@/store/ideContainer';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
@ -34,6 +35,8 @@ const getNodeComponent = (nodeType: string) => {
return SwitchNode; return SwitchNode;
case 'IMAGE': case 'IMAGE':
return ImageNode; return ImageNode;
case 'CODE':
return CodeNode;
default: default:
return LocalNode; return LocalNode;
} }

@ -0,0 +1,185 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue, isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiIns.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.desc}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.desc}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div
className={styles['node-split-line']}
>
</div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{`${output.id} ${output.dataType}` || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{/* 根据节点类型渲染不同的句柄 */}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less'; import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react'; import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue } from '@/utils/common'; import { deserializeValue, isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n'; import cronstrue from 'cronstrue/i18n';
interface NodeContentData { interface NodeContentData {
@ -192,7 +192,7 @@ const formatFooter = (data: any) => {
return cronstrue.toString(intervalSeconds, { locale: 'zh_CN' }); return cronstrue.toString(intervalSeconds, { locale: 'zh_CN' });
case 'EVENTSEND': case 'EVENTSEND':
case 'EVENTLISTENE': case 'EVENTLISTENE':
const { name } = data.customDef; const { name } = isJSON(data.customDef) ? JSON.parse(data.customDef) : data.customDef;
return `事件: ${name}`; return `事件: ${name}`;
default: default:
return '这个类型还没开发'; return '这个类型还没开发';

@ -84,6 +84,28 @@ const imageParameters = {
}], }],
dataOuts: [] dataOuts: []
}; };
const codeParameters = {
apiIns: [{
name: 'start',
desc: '',
dataType: '',
defaultValue: ''
}],
apiOuts: [{
name: 'done',
desc: '',
dataType: '',
defaultValue: ''
}],
dataIns: [],
dataOuts: [{
'arrayType': null,
'dataType': 'STRING',
'defaultValue': 'STRING',
'desc': '输出参数',
'id': 'arg'
}]
};
// 定义节点基本信息 画布中添加的组件列表依赖这里 // 定义节点基本信息 画布中添加的组件列表依赖这里
const nodeDefinitions = [ const nodeDefinitions = [
@ -121,6 +143,9 @@ export const localNodeData = nodeDefinitions.map(({ nodeName, nodeType, nodeGrou
else if (nodeType === 'IMAGE') { else if (nodeType === 'IMAGE') {
parameters = imageParameters; parameters = imageParameters;
} }
else if (nodeType === 'CODE') {
parameters = codeParameters;
}
return { return {
nodeName, nodeName,

Loading…
Cancel
Save