|
|
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
|
|
import { Input, Menu, Tabs } from '@arco-design/web-react';
|
|
|
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
|
|
|
import { useSelector } from 'react-redux';
|
|
|
import { IconSearch } from '@arco-design/web-react/icon';
|
|
|
|
|
|
const TabPane = Tabs.TabPane;
|
|
|
|
|
|
interface AddNodeMenuProps {
|
|
|
onAddNode: (nodeType: string, node: any) => void;
|
|
|
position?: { x: number; y: number }; // 用于画布上下文菜单
|
|
|
edgeId?: string; // 用于边上下文菜单
|
|
|
}
|
|
|
|
|
|
const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
|
|
|
onAddNode
|
|
|
}) => {
|
|
|
const { projectComponentData, info } = useSelector((state: any) => state.ideContainer);
|
|
|
const [groupedNodes, setGroupedNodes] = useState<Record<string, any[]>>({});
|
|
|
const [activeTab, setActiveTab] = useState('common');
|
|
|
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
|
|
|
|
|
|
const basicNodeType = new Map([
|
|
|
['normal', 'BASIC'],
|
|
|
['loop', 'BASIC_LOOP'] // 这个不是基础组件的loop
|
|
|
]);
|
|
|
|
|
|
// 按分组组织节点数据
|
|
|
const formattedNodes = useCallback(() => {
|
|
|
// 先复制本地节点数据
|
|
|
const initialGroupedNodes = localNodeData.reduce((acc, node) => {
|
|
|
if (!acc[node.nodeGroup]) {
|
|
|
acc[node.nodeGroup] = [];
|
|
|
}
|
|
|
acc[node.nodeGroup].push(node);
|
|
|
return acc;
|
|
|
}, {} as Record<string, typeof localNodeData>);
|
|
|
|
|
|
const projectComponent = projectComponentData[info.id];
|
|
|
if (projectComponent) {
|
|
|
const projectComp = projectComponent.projectCompDto;
|
|
|
const projectFlow = projectComponent.projectFlowDto;
|
|
|
|
|
|
const projectCompList = projectComp && Object.values(projectComp).flat(2) || [];
|
|
|
const projectFlowList = projectFlow && Object.values(projectFlow).flat(2) || [];
|
|
|
|
|
|
initialGroupedNodes['application'] = projectCompList.map((v: any) => {
|
|
|
return {
|
|
|
...v,
|
|
|
nodeName: v.name,
|
|
|
nodeType: basicNodeType.get(v.type) || 'BASIC',
|
|
|
nodeGroup: 'application',
|
|
|
data: {
|
|
|
parameters: {
|
|
|
apiIns: v.def?.apis || [],
|
|
|
apiOuts: Array.isArray(v.def?.apiOut) ? v.def?.apiOut : (v.def?.apiOut ? [v.def?.apiOut] : []),
|
|
|
dataIns: v.def?.dataIns || [],
|
|
|
dataOuts: v.def?.dataOuts || []
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
});
|
|
|
initialGroupedNodes['composite'] = projectFlowList.map((v: any) => {
|
|
|
return {
|
|
|
...v,
|
|
|
nodeName: v?.main?.name || '子流程',
|
|
|
nodeType: 'SUB',
|
|
|
nodeGroup: 'composite',
|
|
|
data: {
|
|
|
parameters: {
|
|
|
apiIns: [{ id: 'start', desc: '', dataType: '', defaultValue: '' }],
|
|
|
apiOuts: [{ id: 'done', desc: '', dataType: '', defaultValue: '' }],
|
|
|
dataIns: v.flowHousVO.dataIns,
|
|
|
dataOuts: v.flowHousVO.dataOuts
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
// 更新状态以触发重新渲染
|
|
|
setGroupedNodes(initialGroupedNodes);
|
|
|
}, [projectComponentData, info.id]);
|
|
|
|
|
|
// 根据搜索值过滤节点
|
|
|
const filteredGroupedNodes = useMemo(() => {
|
|
|
if (!searchValue) return groupedNodes;
|
|
|
|
|
|
const filteredNodes: Record<string, any[]> = {};
|
|
|
|
|
|
Object.keys(groupedNodes).forEach(group => {
|
|
|
const nodes = groupedNodes[group];
|
|
|
const filtered = nodes.filter(node => {
|
|
|
const nodeName = node.nodeName || node.name || '';
|
|
|
return nodeName.toLowerCase().includes(searchValue.toLowerCase());
|
|
|
});
|
|
|
|
|
|
if (filtered.length > 0) {
|
|
|
filteredNodes[group] = filtered;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return filteredNodes;
|
|
|
}, [groupedNodes, searchValue]);
|
|
|
|
|
|
// 获取第一个有数据的tab
|
|
|
const getFirstAvailableTab = useCallback(() => {
|
|
|
const groupKeys = Object.keys(filteredGroupedNodes);
|
|
|
if (groupKeys.length > 0) {
|
|
|
return groupKeys[0];
|
|
|
}
|
|
|
return 'common';
|
|
|
}, [filteredGroupedNodes]);
|
|
|
|
|
|
// 当搜索值改变时,自动选中第一个有结果的tab
|
|
|
useEffect(() => {
|
|
|
if (searchValue) {
|
|
|
const firstTab = getFirstAvailableTab();
|
|
|
setActiveTab(firstTab);
|
|
|
}
|
|
|
else {
|
|
|
// 如果搜索值为空,恢复默认选中tab
|
|
|
setActiveTab('common');
|
|
|
}
|
|
|
}, [searchValue, getFirstAvailableTab]);
|
|
|
|
|
|
const handleAddNode = (nodeType: string, node: any) => {
|
|
|
onAddNode(nodeType, node);
|
|
|
};
|
|
|
|
|
|
// 处理搜索输入变化
|
|
|
const handleSearchChange = (value: string) => {
|
|
|
setSearchValue(value);
|
|
|
};
|
|
|
|
|
|
// 分组名称映射
|
|
|
const groupNames: Record<string, string> = {
|
|
|
'application': '基础组件',
|
|
|
'composite': '子流程',
|
|
|
'common': '系统组件'
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
formattedNodes();
|
|
|
}, [formattedNodes]);
|
|
|
|
|
|
const handleTabChange = (key: string) => {
|
|
|
setActiveTab(key);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
style={{
|
|
|
backgroundColor: '#ffffff',
|
|
|
padding: '10px',
|
|
|
borderRadius: '10px',
|
|
|
maxHeight: '400px',
|
|
|
display: 'flex',
|
|
|
flexDirection: 'column'
|
|
|
}}
|
|
|
>
|
|
|
<Input
|
|
|
size="large"
|
|
|
placeholder="搜索节点"
|
|
|
prefix={<IconSearch />}
|
|
|
style={{ marginRight: 16 }}
|
|
|
value={searchValue}
|
|
|
onChange={handleSearchChange}
|
|
|
/>
|
|
|
<Tabs activeTab={activeTab} style={{ flex: '0 0 auto' }} onChange={handleTabChange}>
|
|
|
{Object.entries(filteredGroupedNodes).map(([group, nodes]) => (
|
|
|
<TabPane key={group} title={groupNames[group] || group}>
|
|
|
{/* 只有在当前 tab 激活时才渲染内容 */}
|
|
|
{activeTab === group && (
|
|
|
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
|
<Menu
|
|
|
style={{
|
|
|
border: '1px solid #e4e7ed',
|
|
|
borderRadius: 4,
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
|
|
|
}}
|
|
|
mode="vertical"
|
|
|
hasCollapseButton={false}
|
|
|
>
|
|
|
{nodes && nodes.map((node) => (
|
|
|
<Menu.Item
|
|
|
key={node.nodeType + (node.id || '')}
|
|
|
onClick={() => handleAddNode(node.nodeType, node)}
|
|
|
style={{
|
|
|
padding: '0 16px',
|
|
|
height: 36,
|
|
|
lineHeight: '36px'
|
|
|
}}
|
|
|
>
|
|
|
{node.nodeName}
|
|
|
</Menu.Item>
|
|
|
))}
|
|
|
</Menu>
|
|
|
</div>
|
|
|
)}
|
|
|
</TabPane>
|
|
|
))}
|
|
|
</Tabs>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default AddNodeMenu; |