You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
11 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue, formatDataType, isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
import { useSelector } from 'react-redux';
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 renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
const renderStartNodeHandles = () => {
if (!isStartNode) return null;
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`start-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || `start-output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{dataOuts.length > 0 && dataOuts.map((_, index) => (
<Handle
key={`output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name|| dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + apiOuts.length * 20 + index * 20}px`
}}
/>
))}
</>
);
};
const renderEndNodeHandles = () => {
if (!isEndNode) return null;
return (
<>
{apiIns.map((_, index) => (
<Handle
key={`end-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || `end-input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{dataIns.length > 0 && dataIns.map((_, index) => (
<Handle
key={`input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + apiIns.length * 20 + index * 20}px`
}}
/>
))}
</>
);
};
return (
<>
{renderStartNodeHandles()}
{renderEndNodeHandles()}
</>
);
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
// 计算 API 占位数量:如果没有 API 句柄,默认占位 1 个
const apiInsPlaceholder = Math.max(apiIns.length, 1);
const apiOutsPlaceholder = Math.max(apiOuts.length, 1);
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: `${37 + index * 22}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: `${37 + index * 22}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: `${65 + (apiInsPlaceholder + index) * 22}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: `${65 + (apiOutsPlaceholder + index) * 22}px`
}}
/>
))}
</>
);
};
const formatFooter = (data: any, eventListOld = []) => {
try {
switch (data?.type) {
case 'WAIT':
const { duration } = deserializeValue(data.customDef);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
return `${hours}小时${minutes}分钟${seconds}`;
case 'CYCLE':
const { intervalSeconds } = deserializeValue(data.customDef);
return cronstrue.toString(intervalSeconds, { locale: 'zh_CN' });
case 'EVENTSEND':
case 'EVENTLISTENE':
const parsedData = isJSON(data.customDef) ? JSON.parse(data.customDef) : null;
// 数据是JSON字符串标识是接口回来的数据是普通对象标识是当前操作
if (parsedData) {
const { eventId, topic, name } = parsedData;
if (topic.includes('**empty**')) return '';
const currentEvent = eventListOld.length > 0 ? eventListOld.find(item => item.eventId === eventId) : { name: '无' };
return `事件: ${currentEvent.name}`;
}
else {
const { name, topic } = data.customDef;
if (topic.includes('**empty**')) return '';
return `事件: ${name}`;
}
case 'EVENTSEND_SYNC':
const parsedData1 = isJSON(data.customDef) ? JSON.parse(data.customDef) : null;
if (parsedData1) {
const { eventName } = parsedData1;
return `事件: ${eventName}`;
}
else {
return '';
}
case 'BASIC':
case 'BASIC_LOOP':
return data.compIdentifier ? `当前实例:${data.compIdentifier}` : '';
default:
return '';
}
} catch (e) {
console.log(e);
}
};
const formatTitle = (text) => {
return text === 'start' || text === 'end' ? '' : text;
};
const NodeContentLocal = ({ data }: { data: NodeContentData }) => {
const { eventListOld } = useSelector((state) => state.ideContainer);
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = formatFooter(data.component) || false;
const footerData = (showFooter && data.component) || {};
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
const isSpecialNode = isStartNode || isEndNode;
// 判断是否需要显示 API 区域(即使为空也显示占位)
const hasApiSection = apiIns.length > 0 || apiOuts.length > 0 || dataIns.length > 0 || dataOuts.length > 0;
return (
<>
{/*content栏-api部分 - 始终显示以保持布局一致*/}
{hasApiSection && (
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{(apiIns.length > 0 || dataIns.length > 0) && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{formatTitle(input.desc || input.id || input.name)}
</div>
))}
{/* 如果没有 API 输入但有数据输入,添加占位 */}
{apiIns.length === 0 && dataIns.length > 0 && (
<div className={styles['node-input-label']} style={{ visibility: 'hidden' }}>
</div>
)}
</div>
)}
{(apiOuts.length > 0 || dataOuts.length > 0) && (
<div className={styles['node-outputs']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-output-label']}>
{output.desc}
</div>
))}
{/* 如果没有 API 输出但有数据输出,添加占位 */}
{apiOuts.length === 0 && dataOuts.length > 0 && (
<div className={styles['node-output-label']} style={{ visibility: 'hidden' }}>
</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']}>
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
<span
className={styles['node-data-type']}
>{input.id || `输入${index + 1}`} {formatDataType(input.dataType)}
</span>
</div>
))}
</div>
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-output-label']}>
<span
className={styles['node-data-type']}
>{formatDataType(output.dataType)} {output.id || `输出${index + 1}`}
</span>
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{/*footer栏*/}
{showFooter && (
<div className={styles['node-footer']}>
{formatFooter(footerData, eventListOld)}
</div>
)}
{/* 根据节点类型渲染不同的句柄 */}
{isSpecialNode
? renderSpecialNodeHandles(isStartNode, isEndNode, dataIns, dataOuts, apiIns, apiOuts)
: renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContentLocal;