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.

209 lines
6.6 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, { 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;