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.
963 lines
30 KiB
TypeScript
963 lines
30 KiB
TypeScript
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
|
import styles from './style/sideBar.module.less';
|
|
import {
|
|
ResizeBox,
|
|
Tree,
|
|
Input,
|
|
Button,
|
|
Modal,
|
|
Form,
|
|
Message,
|
|
Dropdown,
|
|
Menu,
|
|
Popconfirm
|
|
} from '@arco-design/web-react';
|
|
import {
|
|
IconApps,
|
|
IconMore,
|
|
IconDelete,
|
|
IconEdit,
|
|
IconEye,
|
|
IconSearch,
|
|
IconPlus,
|
|
IconEyeInvisible,
|
|
IconExpand,
|
|
IconShrink,
|
|
IconLeft,
|
|
IconLoading
|
|
} from '@arco-design/web-react/icon';
|
|
import { menuData1, menuData2 } from './config/menuData';
|
|
import { Selected } from '@/pages/ideContainer/types';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import {
|
|
updateMenuData,
|
|
updateFlowData,
|
|
updateCanvasDataMap,
|
|
updateCurrentAppData,
|
|
updateEventListOld,
|
|
updateEventNodeList,
|
|
updateIsRunning,
|
|
updateRuntimeId
|
|
} from '@/store/ideContainer';
|
|
import { addApp, getProjectEnv, editApp, deleteApp } from '@/api/apps';
|
|
import _ from 'lodash';
|
|
import { getAppInfoNew } from '@/api/appRes';
|
|
import { getAppEventData } from '@/api/appEvent';
|
|
import { queryEventItemBySceneIdOld } from '@/api/event';
|
|
import { convertFlowData } from '@/utils/convertFlowData';
|
|
|
|
const TreeNode = Tree.Node;
|
|
const FormItem = Form.Item;
|
|
const TextArea = Input.TextArea;
|
|
const MenuItem = Menu.Item;
|
|
|
|
interface MenuItemType {
|
|
title: string;
|
|
key?: string;
|
|
children?: MenuItemType[];
|
|
path?: string;
|
|
icon?: React.ReactNode;
|
|
|
|
[key: string]: string | MenuItemType[] | React.ReactNode;
|
|
}
|
|
|
|
interface SideBarProps {
|
|
subMenuData: any;
|
|
selected?: any;
|
|
identity?: string;
|
|
showSubMenu: boolean;
|
|
onMenuSelect?: (selected: Selected) => void;
|
|
onRefresh: () => void;
|
|
onDeleteApp?: (appId: string) => void; // 添加删除应用的回调函数
|
|
}
|
|
|
|
const compTypeMap = {
|
|
appComponent: '普通组件',
|
|
subComponent: '子流程'
|
|
};
|
|
|
|
|
|
const AppHandleModal = ({ appInfo, visible, type, onChangeVisible, onClose, onRefresh }) => {
|
|
const { info } = useSelector(state => state.ideContainer);
|
|
const [form] = Form.useForm();
|
|
|
|
useEffect(() => {
|
|
if (type === 'EDIT' && appInfo && visible) {
|
|
form.setFieldsValue({
|
|
id: appInfo?.id,
|
|
name: appInfo?.name,
|
|
description: appInfo?.description,
|
|
published: appInfo?.published === 1,
|
|
logo: 'scene04.png', // 先写死
|
|
sceneId: info.id
|
|
});
|
|
}
|
|
}, [type, appInfo, visible, form, info.id]);
|
|
|
|
const onOk = async () => {
|
|
await form.validate();
|
|
const formData = form.getFields();
|
|
|
|
// 新增
|
|
if (type === 'ADD') {
|
|
// 新增应用的入参
|
|
const params = {
|
|
description: formData.description,
|
|
name: formData.name,
|
|
logo: 'scene04.png', // 先写死
|
|
published: 0,
|
|
sceneId: info.id
|
|
};
|
|
const res: any = await addApp(params);
|
|
if (res.code === 200) Message.success('创建成功');
|
|
else Message.error(res.message);
|
|
}
|
|
// 编辑
|
|
else {
|
|
const res: any = await editApp(formData);
|
|
if (res.code === 200) Message.success('编辑成功');
|
|
else Message.error(res.message);
|
|
}
|
|
|
|
// 清空表单数据和其他变量数据
|
|
form.resetFields();
|
|
onRefresh();
|
|
onClose();
|
|
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
title={type === 'ADD' ? '新增应用' : '编辑应用'}
|
|
visible={visible}
|
|
onOk={() => onOk()}
|
|
onCancel={() => onChangeVisible(false)}
|
|
autoFocus={false}
|
|
focusLock={true}
|
|
maskClosable={false}
|
|
>
|
|
<Form form={form} autoComplete="off">
|
|
<FormItem
|
|
field="name"
|
|
label="应用名称"
|
|
required
|
|
rules={[
|
|
{
|
|
validator(value, cb) {
|
|
if (!value) {
|
|
return cb('请填写应用名称');
|
|
}
|
|
// 检查是否只包含汉字
|
|
const chineseOnlyRegex = /^[\u4e00-\u9fa5]+$/;
|
|
if (!chineseOnlyRegex.test(value)) {
|
|
return cb('应用名称只能输入汉字');
|
|
}
|
|
|
|
return cb();
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
<Input placeholder="请输入应用名称" />
|
|
</FormItem>
|
|
<FormItem
|
|
field="description"
|
|
label="应用描述"
|
|
required
|
|
rules={[
|
|
{
|
|
validator(value, cb) {
|
|
if (!value) {
|
|
return cb('请填写应用描述');
|
|
}
|
|
|
|
return cb();
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
{/*<Input placeholder="请输入工程描述" />*/}
|
|
<TextArea placeholder="请输入应用描述" />
|
|
</FormItem>
|
|
</Form>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
const SideBar: React.FC<SideBarProps> = ({
|
|
selected,
|
|
identity,
|
|
subMenuData,
|
|
showSubMenu,
|
|
onMenuSelect,
|
|
onRefresh,
|
|
onDeleteApp
|
|
}) => {
|
|
const [menu, setMenu] = useState<MenuItemType[]>([]);
|
|
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
|
|
const [subMenuWidth, setSubMenuWidth] = useState(300); // 子菜单宽度状态
|
|
const [isSubMenuCollapsed, setIsSubMenuCollapsed] = useState(false); // 子菜单收起状态
|
|
const [activeKey, setActiveKey] = useState(0);
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [modalType, setModalType] = useState<'ADD' | 'EDIT'>('ADD');
|
|
const [currentApp, setCurrentApp] = useState<any>(null);
|
|
const [contextMenu, setContextMenu] = useState<{
|
|
visible: boolean;
|
|
x: number;
|
|
y: number;
|
|
nodeData: any;
|
|
}>({
|
|
visible: false,
|
|
x: 0,
|
|
y: 0,
|
|
nodeData: null
|
|
}); // 添加右键菜单状态
|
|
// 用于存储隐藏的节点ID
|
|
const [hiddenNodes, setHiddenNodes] = useState<Set<string>>(new Set());
|
|
// 用于控制展开的节点
|
|
const [expandedKeys, setExpandedKeys] = useState<string[]>(['0']);
|
|
// 用于标记是否全部展开
|
|
const [isAllExpanded, setIsAllExpanded] = useState(false);
|
|
const resizeBoxRef = useRef<HTMLDivElement>(null); // 引用第一个 ResizeBox 容器
|
|
const contextMenuRef = useRef<HTMLDivElement>(null); // 右键菜单引用
|
|
const { menuData, info, canvasDataMap } = useSelector(state => state.ideContainer);
|
|
const dispatch = useDispatch();
|
|
|
|
function getMenuData(): MenuItemType[] {
|
|
switch (identity) {
|
|
case 'scene':
|
|
const menuData = _.cloneDeep(menuData1);
|
|
const newSubMenuData = _.cloneDeep(subMenuData);
|
|
|
|
// 遍历所有 subMenuKey 来构建子菜单数据结构
|
|
Object.keys(newSubMenuData).forEach(subMenuKey => {
|
|
const subMenuValue: any = _.cloneDeep(newSubMenuData[subMenuKey]);
|
|
const menuIndex = menuData.findIndex(v => v.key === subMenuKey);
|
|
|
|
if (menuIndex !== -1) {
|
|
// 构建数据结构,这是目录默认需要的数据结构
|
|
subMenuValue.forEach(v => {
|
|
v.title = v.name;
|
|
v.key = `compFlow-${v.id}`;
|
|
v.path = 'compFlow';
|
|
v.parentKey = subMenuKey;
|
|
v.icon = '/ideContainer/icon/app.png';
|
|
if (subMenuKey !== 'appFlow') {
|
|
v.children = [
|
|
{
|
|
title: '事件',
|
|
children: null,
|
|
icon: '/ideContainer/icon/list.png'
|
|
},
|
|
{
|
|
title: '组件列表',
|
|
children: null,
|
|
icon: '/ideContainer/icon/list.png'
|
|
}
|
|
];
|
|
}
|
|
});
|
|
menuData[menuIndex]['children'] = subMenuValue;
|
|
}
|
|
});
|
|
|
|
dispatch(updateMenuData({ 'scene': menuData }));
|
|
return menuData;
|
|
case 'componentDevelopment' :
|
|
dispatch(updateMenuData({ 'componentDevelopment': menuData2 }));
|
|
return menuData2;
|
|
default:
|
|
return menuData1;
|
|
}
|
|
}
|
|
|
|
// 监听 subMenuData 和 identity 变化,更新菜单数据
|
|
useEffect(() => {
|
|
if (identity === 'scene') {
|
|
if (Object.keys(subMenuData).length > 0) {
|
|
const newMenu = getMenuData();
|
|
setMenu(newMenu);
|
|
}
|
|
}
|
|
else if (identity === 'componentDevelopment') {
|
|
const newMenu = getMenuData();
|
|
setMenu(newMenu);
|
|
}
|
|
}, [subMenuData, identity]);
|
|
|
|
// 处理子菜单区域的拖拽调整大小
|
|
const handleSubMenuResize = (e: MouseEvent, { width }: { width: number }) => {
|
|
resizeBoxRef.current.style.width = `${width}px`;
|
|
};
|
|
|
|
// 切换子菜单区域的收起/展开状态
|
|
const toggleSubMenu = () => {
|
|
setIsSubMenuCollapsed(prev => !prev);
|
|
};
|
|
|
|
// 监听子菜单收起/展开状态变化,更新宽度
|
|
useEffect(() => {
|
|
if (resizeBoxRef.current) {
|
|
const width = isSubMenuCollapsed ? 0 : 300;
|
|
resizeBoxRef.current.style.width = `${width}px`;
|
|
}
|
|
}, [isSubMenuCollapsed]);
|
|
|
|
// 获取所有可展开节点的 keys
|
|
const getAllExpandableKeys = useCallback((menuItems?: MenuItemType[], parentKey = '0'): string[] => {
|
|
const keys: string[] = [];
|
|
if (!menuItems || menuItems.length === 0) return keys;
|
|
|
|
menuItems.forEach((item, index) => {
|
|
const key = `${parentKey}-${index}`;
|
|
// 只有有子节点的才需要添加到展开列表
|
|
if (item.children && item.children.length > 0) {
|
|
keys.push(key);
|
|
// 递归获取子节点的 keys
|
|
keys.push(...getAllExpandableKeys(item.children, key));
|
|
}
|
|
});
|
|
|
|
return keys;
|
|
}, []);
|
|
|
|
// 切换展开/折叠全部
|
|
const toggleExpandAll = useCallback(() => {
|
|
if (isAllExpanded) {
|
|
// 折叠全部
|
|
setExpandedKeys([]);
|
|
setIsAllExpanded(false);
|
|
}
|
|
else {
|
|
// 展开全部
|
|
const allKeys = getAllExpandableKeys(filteredMenu[activeKey]?.children);
|
|
setExpandedKeys(allKeys);
|
|
setIsAllExpanded(true);
|
|
}
|
|
}, [isAllExpanded, activeKey, getAllExpandableKeys]);
|
|
|
|
// 处理菜单项点击事件
|
|
const handleMenuItemClick = (item: MenuItemType, index: number) => {
|
|
setActiveKey(index);
|
|
|
|
if (showSubMenu) {
|
|
// 如果点击的是当前已激活的菜单项,则切换子菜单的展开/收起状态
|
|
if ((selected?.parentKey || selected.key) === `${item.key}`) {
|
|
toggleSubMenu();
|
|
}
|
|
else {
|
|
// 如果点击的是其他菜单项,则展开子菜单(如果已收起)
|
|
if (isSubMenuCollapsed) {
|
|
setIsSubMenuCollapsed(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 调用外部传入的菜单选择处理函数
|
|
onMenuSelect?.({ ...item });
|
|
};
|
|
|
|
// 监听 selected 状态变化,更新 activeKey
|
|
useEffect(() => {
|
|
if (menu.length > 0 && selected?.parentKey) {
|
|
// 查找匹配的菜单项索引
|
|
const index = menu.findIndex(item => item.key === selected.parentKey);
|
|
if (index !== -1) {
|
|
setActiveKey(index);
|
|
}
|
|
}
|
|
}, [selected, menu]);
|
|
|
|
// 监听 selected 状态变化,确保子菜单正确显示
|
|
useEffect(() => {
|
|
if (selected?.key && menuData[identity]) {
|
|
// 确保子菜单区域根据selected状态正确展开或收起
|
|
if (['appList', 'appFlow'].includes(selected.parentKey)) {
|
|
// // 如果应该显示子菜单但当前是收起状态,则展开
|
|
// if (isSubMenuCollapsed) {
|
|
// setIsSubMenuCollapsed(false);
|
|
// }
|
|
}
|
|
|
|
// 强制更新filteredMenu以确保显示正确的子菜单
|
|
setSearchValue(''); // 清空搜索值以显示所有项
|
|
}
|
|
}, [selected, menuData, identity]);
|
|
|
|
// 当菜单数据变化时更新本地状态
|
|
useEffect(() => {
|
|
if (identity && menuData[identity]) {
|
|
setMenu(menuData[identity]);
|
|
}
|
|
}, [menuData, identity]);
|
|
|
|
|
|
// 根据搜索值过滤菜单数据
|
|
const filteredMenu = useMemo(() => {
|
|
if (!searchValue || activeKey === undefined || !menu[activeKey]?.children) {
|
|
return menu;
|
|
}
|
|
|
|
// 深拷贝菜单数据以避免修改原始数据
|
|
const menuCopy = _.cloneDeep(menu);
|
|
|
|
// 只对当前激活的菜单项进行过滤
|
|
if (menuCopy[activeKey]?.children) {
|
|
menuCopy[activeKey].children = menuCopy[activeKey].children.filter((item: MenuItemType) => {
|
|
// 检查当前项是否匹配搜索条件
|
|
const title = item.title || '';
|
|
const isMatch = title.toLowerCase().includes(searchValue.toLowerCase());
|
|
|
|
// 如果当前项不匹配,检查其子项是否匹配
|
|
if (!isMatch && item.children && item.children.length > 0) {
|
|
item.children = item.children.filter(child => {
|
|
const childTitle = child.title || '';
|
|
return childTitle.toLowerCase().includes(searchValue.toLowerCase());
|
|
});
|
|
|
|
// 如果有匹配的子项,或者当前项匹配,则保留该项
|
|
return item.children.length > 0;
|
|
}
|
|
|
|
return isMatch;
|
|
});
|
|
}
|
|
|
|
return menuCopy;
|
|
}, [menu, searchValue, activeKey]);
|
|
|
|
const getProjectEnvData = async (data) => {
|
|
if (!data.path || !data.key || data.hasOwnProperty('compData')) return;
|
|
const parentKey = menu[activeKey]?.key;
|
|
const currentMenu = _.cloneDeep(menuData[identity]);
|
|
const index = currentMenu.findIndex(v => v.key === parentKey);
|
|
const res: any = await getAppInfoNew(data.id);
|
|
getAppEventData(data.id);
|
|
if (res.code === 200) {
|
|
const children = currentMenu[index].children.find(v => v.id === data.id);
|
|
children.children[0].children = res.data.events.map(item => {
|
|
return {
|
|
title: item,
|
|
children: null,
|
|
icon: '/ideContainer/icon/list.png'
|
|
};
|
|
});
|
|
children.children[1].children = Object.keys(res.data.compList).map(item => {
|
|
// 对于普通组件,直接渲染组件名称列表
|
|
if (item === 'appComponent') {
|
|
return {
|
|
title: compTypeMap[item],
|
|
icon: '/ideContainer/icon/app1.png',
|
|
children: res.data.compList[item].map(title => {
|
|
return {
|
|
title: title,
|
|
children: null,
|
|
icon: '/ideContainer/icon/tool.png'
|
|
};
|
|
})
|
|
};
|
|
}
|
|
// 对于复合组件,直接渲染复合组件本身(不渲染子流程)
|
|
else {
|
|
return {
|
|
title: compTypeMap[item],
|
|
icon: '/ideContainer/icon/complexApp.png',
|
|
children: res.data.subs.map(info => {
|
|
return {
|
|
title: info.flowName,
|
|
children: null,
|
|
icon: '/ideContainer/icon/tool.png',
|
|
compData: info,
|
|
path: 'complexFlow',
|
|
key: info.flowId,
|
|
pathTitle: `${data.title} / ${info.flowName}`,
|
|
parentKey: 'appList',
|
|
parentAppId: data.id
|
|
};
|
|
})
|
|
};
|
|
}
|
|
});
|
|
|
|
const findMenuItem = (menuItems: any[], key: string): any => {
|
|
for (const item of menuItems) {
|
|
if (item.key === key) {
|
|
return item;
|
|
}
|
|
if (item.children) {
|
|
const found = findMenuItem(item.children, key);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
|
|
// 更新 menuData 中的数据
|
|
dispatch(updateMenuData({ ...menuData, [identity]: currentMenu }));
|
|
// 更新 flowData 中的数据
|
|
dispatch(updateFlowData({ [data.id]: res.data }));
|
|
// 更新 currentAppData 中的数据
|
|
dispatch(updateCurrentAppData({ ...findMenuItem(menuData[identity], children.key) }));
|
|
dispatch(updateEventNodeList({
|
|
eventSendNodeList: [],
|
|
eventlisteneList: []
|
|
}));
|
|
|
|
// 同步更新到 canvasDataMap
|
|
if (res.data.main?.components) {
|
|
const { nodes, edges } = convertFlowData(res.data.main.components, true);
|
|
dispatch(updateCanvasDataMap({
|
|
...canvasDataMap,
|
|
[data.id]: { nodes, edges }
|
|
}));
|
|
}
|
|
|
|
// 同时更新本地 menu 状态以触发重新渲染
|
|
setMenu(prevMenu => {
|
|
const newMenu = [...prevMenu];
|
|
newMenu[activeKey] = { ...newMenu[activeKey], children: currentMenu[index].children };
|
|
return newMenu;
|
|
});
|
|
// 获取最新的事件枚举表
|
|
const res1: any = await queryEventItemBySceneIdOld(info.id);
|
|
if (res1.code === 200) dispatch(updateEventListOld(res1.data));
|
|
}
|
|
};
|
|
|
|
// 渲染子菜单
|
|
const renderMenuItems = (menuItems?: MenuItemType[], parentKey = '0') => {
|
|
if (menuItems && menuItems.length) {
|
|
return menuItems.map((item, index) => {
|
|
const key = `${parentKey}-${index}`;
|
|
const treeNodeProps = {
|
|
title: item.title,
|
|
key: key,
|
|
dataRef: item // 传递原始数据,包含 path、key 等信息
|
|
};
|
|
if (item.children && item.children.length > 0) {
|
|
return (
|
|
<TreeNode
|
|
{...treeNodeProps}
|
|
title={item.title}
|
|
key={key}
|
|
icon={item?.icon ? <img src={item.icon as string} style={{ width: 14, height: 14 }} /> : null}
|
|
>
|
|
{renderMenuItems(item.children, key)}
|
|
</TreeNode>
|
|
);
|
|
}
|
|
else {
|
|
if (item.title.includes('隐藏事件')) return null;
|
|
return (<TreeNode
|
|
{...treeNodeProps}
|
|
title={item.title}
|
|
key={key}
|
|
icon={item?.icon ? <img src={item.icon as string} style={{ width: 14, height: 14 }} /> : null}
|
|
/>);
|
|
}
|
|
});
|
|
}
|
|
return null;
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (identity === 'scene') Object.keys(subMenuData).length > 0 && setMenu(getMenuData());
|
|
else if (identity === 'componentDevelopment') setMenu(getMenuData());
|
|
}, [subMenuData, identity]);
|
|
|
|
// 处理搜索输入变化
|
|
const handleSearchChange = (value: string) => {
|
|
setSearchValue(value);
|
|
};
|
|
|
|
// 处理鼠标按下事件
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
// 明确检查右键点击
|
|
if (e.button === 2) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
// 获取点击的目标元素及其数据
|
|
const target = e.target as HTMLElement;
|
|
const treeNode = target.closest('.arco-tree-node');
|
|
if (treeNode) {
|
|
// 在实际应用中,你可能需要通过 tree-node 获取对应的数据
|
|
// 这里我们简单地显示右键菜单
|
|
}
|
|
|
|
// 设置右键菜单的位置和显示
|
|
setContextMenu({
|
|
visible: true,
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
nodeData: null
|
|
});
|
|
}
|
|
};
|
|
|
|
// 劫持右键
|
|
const handleContextMenu = (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// 设置右键菜单的位置和显示
|
|
setContextMenu({
|
|
visible: true,
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
nodeData: null
|
|
});
|
|
};
|
|
|
|
// 点击其他地方隐藏右键菜单
|
|
useEffect(() => {
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) {
|
|
setContextMenu(prev => ({ ...prev, visible: false }));
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
// 监听自定义事件以更新隐藏节点状态
|
|
useEffect(() => {
|
|
const handleToggleNodeVisibility = (event: CustomEvent) => {
|
|
const { appId, isVisible } = event.detail;
|
|
|
|
if (isVisible) {
|
|
// 显示节点 - 从隐藏节点集合中移除
|
|
setHiddenNodes(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(appId);
|
|
return newSet;
|
|
});
|
|
}
|
|
else {
|
|
// 隐藏节点 - 添加到隐藏节点集合
|
|
setHiddenNodes(prev => new Set(prev).add(appId));
|
|
}
|
|
};
|
|
|
|
// 监听导航到Tab的事件
|
|
const handleNavigateToTab = (event: CustomEvent) => {
|
|
const { path } = event.detail;
|
|
|
|
// 查找对应的菜单项
|
|
const menuItems = menuData[identity];
|
|
if (!menuItems) return;
|
|
|
|
const findMenuItem = (items: any[]): any => {
|
|
for (const item of items) {
|
|
if (item.path === path) {
|
|
return item;
|
|
}
|
|
if (item.children) {
|
|
const found = findMenuItem(item.children);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const menuItem = findMenuItem(menuItems);
|
|
if (menuItem) {
|
|
// 触发菜单选择
|
|
onMenuSelect?.({ ...menuItem });
|
|
}
|
|
};
|
|
|
|
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
|
|
document.addEventListener('navigateToTab', handleNavigateToTab as EventListener);
|
|
|
|
return () => {
|
|
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
|
|
document.removeEventListener('navigateToTab', handleNavigateToTab as EventListener);
|
|
};
|
|
}, [menuData, identity, onMenuSelect]);
|
|
|
|
// 渲染节点的额外操作按钮
|
|
const renderNodeExtra = (node) => {
|
|
// 只有当 node.dataRef.id 存在时才渲染操作按钮
|
|
if (!node.dataRef?.id) return null;
|
|
|
|
const dropList = (
|
|
<Menu>
|
|
<MenuItem
|
|
key="editApp"
|
|
onClick={() => {
|
|
setCurrentApp(node.dataRef);
|
|
setModalType('EDIT');
|
|
setShowModal(true);
|
|
setContextMenu(prev => ({ ...prev, visible: false })); // 隐藏右键菜单
|
|
}}
|
|
>
|
|
<span style={{ color: 'rgba(161, 165, 194, 1)' }}>
|
|
<IconEdit style={{ marginRight: 6 }} />
|
|
编辑信息
|
|
</span>
|
|
</MenuItem>
|
|
<MenuItem
|
|
key="deleteApp"
|
|
onClick={() => {
|
|
// 使用Hook方式调用
|
|
Modal.confirm({
|
|
title: '请确认是否删除该应用?',
|
|
content: '应用删除后无法恢复,请谨慎删除!',
|
|
maskClosable: false,
|
|
cancelText: '取消',
|
|
simple: false,
|
|
closable: false,
|
|
onOk: async () => {
|
|
await deleteApp(node.dataRef.id);
|
|
// 通知父组件应用已被删除
|
|
onDeleteApp?.(node.dataRef.id);
|
|
onRefresh();
|
|
setContextMenu(prev => ({ ...prev, visible: false })); // 隐藏右键菜单
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<span style={{ color: 'rgba(255, 82, 25, 1)' }}>
|
|
<IconDelete style={{ marginRight: 6 }} />
|
|
删除应用
|
|
</span>
|
|
</MenuItem>
|
|
</Menu>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{node?.dataRef?.scheduled === 1 && <div
|
|
style={{
|
|
position: 'absolute',
|
|
right: 35,
|
|
top: 8,
|
|
color: '#000000'
|
|
}}
|
|
>
|
|
<IconLoading />
|
|
</div>}
|
|
<Dropdown droplist={dropList} trigger="click">
|
|
<IconMore
|
|
style={{
|
|
position: 'absolute',
|
|
right: 8,
|
|
fontSize: 20,
|
|
fontWeight: 700,
|
|
top: 10,
|
|
color: '#000000'
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
}}
|
|
/>
|
|
</Dropdown>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const renderNodeExtraEye = (node) => {
|
|
// console.log('node:', node);
|
|
// 只有当 node.dataRef.id 存在时才渲染操作按钮
|
|
if (!node.dataRef?.id) return null;
|
|
|
|
// 检查节点当前是否被隐藏
|
|
const isNodeHidden = hiddenNodes?.has(node.dataRef.id);
|
|
|
|
function handleEyeClick(e) {
|
|
e.stopPropagation();
|
|
// 发送自定义事件,通知流程图组件隐藏/显示节点
|
|
const event = new CustomEvent('toggleNodeVisibility', {
|
|
detail: {
|
|
appId: node.dataRef.id,
|
|
isVisible: isNodeHidden // 如果当前是隐藏的,点击后应该显示
|
|
}
|
|
});
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
right: 8,
|
|
fontSize: 20,
|
|
fontWeight: 700,
|
|
top: 10,
|
|
color: '#000000',
|
|
cursor: 'pointer'
|
|
}}
|
|
onClick={handleEyeClick}
|
|
>
|
|
{isNodeHidden ? <IconEye /> : <IconEyeInvisible />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={styles['sider']}
|
|
>
|
|
<div className={styles['menu-container']}>
|
|
{menu.map((item, index) => (
|
|
<div
|
|
key={index}
|
|
className={`${styles['menu-item']} ${(selected.parentKey === `${item.parentKey}`) ? styles['menu-item-active'] : ''}`}
|
|
onClick={() => handleMenuItemClick(item, index)}
|
|
>
|
|
<div className={styles['menu-item-content']}>
|
|
{item.icon &&
|
|
<img src={selected.parentKey === `${item.parentKey}` ? item.activeIcon as string : item.icon as string}
|
|
style={{ width: 20, height: 20, marginBottom: 7 }} />}
|
|
{!item.icon && (
|
|
<span className={styles['menu-item-icon']}>
|
|
{item.icon || <IconApps />}
|
|
</span>
|
|
)}
|
|
<span className={styles['menu-item-text']}>
|
|
{item.title}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{showSubMenu && <ResizeBox
|
|
ref={resizeBoxRef}
|
|
directions={['right']}
|
|
style={{
|
|
width: subMenuWidth,
|
|
height: '100%',
|
|
minHeight: 'calc(100vh - 60px)'
|
|
}}
|
|
onMoving={handleSubMenuResize}
|
|
>
|
|
<div
|
|
className={styles['sub-menu']}
|
|
>
|
|
{/* 搜索/添加 */}
|
|
{menu[activeKey]?.key === 'appList' && <div className={styles['handle-box']}>
|
|
<Input
|
|
prefix={<IconSearch />}
|
|
placeholder={'搜索'}
|
|
style={{ flex: 1 }}
|
|
value={searchValue}
|
|
onChange={handleSearchChange}
|
|
/>
|
|
<Button
|
|
type="secondary"
|
|
icon={isAllExpanded ? <IconShrink /> : <IconExpand />}
|
|
style={{ marginLeft: 5 }}
|
|
onClick={toggleExpandAll}
|
|
title={isAllExpanded ? '折叠全部' : '展开全部'}
|
|
/>
|
|
<Button
|
|
type="primary"
|
|
icon={<IconPlus />}
|
|
style={{ marginLeft: 5 }}
|
|
onClick={() => {
|
|
setShowModal(true);
|
|
setModalType('ADD');
|
|
}}
|
|
/>
|
|
</div>}
|
|
|
|
{/* 子菜单 */}
|
|
<div onContextMenu={handleContextMenu}>
|
|
<Tree
|
|
expandedKeys={expandedKeys}
|
|
onExpand={(keys) => {
|
|
setExpandedKeys(keys as string[]);
|
|
// 检查是否全部展开,更新 isAllExpanded 状态
|
|
const allKeys = getAllExpandableKeys(filteredMenu[activeKey]?.children);
|
|
setIsAllExpanded(keys.length >= allKeys.length && allKeys.length > 0);
|
|
}}
|
|
selectedKeys={[]} // 移除选中样式
|
|
onMouseDown={handleMouseDown}
|
|
onSelect={async (_selectedKeys, info) => {
|
|
const selectedNode = info.node;
|
|
const originalData = selectedNode.props.dataRef;
|
|
if (selected?.parentKey === 'appList') {
|
|
await getProjectEnvData(originalData);
|
|
|
|
// 根据应用的 scheduled 状态更新画布的运行状态
|
|
// scheduled === 1 表示应用正在运行中
|
|
dispatch(updateIsRunning(originalData?.scheduled === 1));
|
|
// 运行中的应用同步更新runId
|
|
dispatch(updateRuntimeId(originalData?.instanceId));
|
|
|
|
// 调用外部传入的菜单选择处理函数
|
|
originalData.key && onMenuSelect?.({ ...originalData } as Selected);
|
|
}
|
|
}}
|
|
style={{ background: 'transparent' }} // 移除背景色
|
|
renderExtra={selected?.parentKey === 'appList' ? renderNodeExtra : renderNodeExtraEye}
|
|
>
|
|
{renderMenuItems(filteredMenu[activeKey]?.children)}
|
|
</Tree>
|
|
</div>
|
|
|
|
{/* 底部折叠按钮 */}
|
|
{!isSubMenuCollapsed && <div
|
|
className={styles['collapse-btn']}
|
|
onClick={toggleSubMenu}
|
|
title="收起侧边栏"
|
|
>
|
|
<IconLeft style={{ fontSize: 16 }} />
|
|
</div>}
|
|
</div>
|
|
</ResizeBox>}
|
|
|
|
{/* 右键菜单 */}
|
|
{/*{contextMenu.visible && (*/}
|
|
{/* <div*/}
|
|
{/* ref={contextMenuRef}*/}
|
|
{/* className={styles['context-menu']}*/}
|
|
{/* style={{*/}
|
|
{/* position: 'fixed',*/}
|
|
{/* top: contextMenu.y,*/}
|
|
{/* left: contextMenu.x,*/}
|
|
{/* zIndex: 1000*/}
|
|
{/* }}*/}
|
|
{/* >*/}
|
|
{/* <Menu*/}
|
|
{/* className={styles['context-menu-dropdown']}*/}
|
|
{/* style={{*/}
|
|
{/* borderRadius: 4,*/}
|
|
{/* boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'*/}
|
|
{/* }}*/}
|
|
{/* >*/}
|
|
{/* <MenuItem*/}
|
|
{/* key="refresh"*/}
|
|
{/* onClick={() => {*/}
|
|
{/* console.log('点击');*/}
|
|
{/* // onRefresh();*/}
|
|
{/* // setContextMenu(prev => ({ ...prev, visible: false }));*/}
|
|
{/* }}*/}
|
|
{/* >*/}
|
|
{/* <span>编辑复合组件</span>*/}
|
|
{/* </MenuItem>*/}
|
|
{/* </Menu>*/}
|
|
{/* </div>*/}
|
|
{/*)}*/}
|
|
|
|
{/* 新增/编辑应用 */}
|
|
{showModal && (
|
|
<AppHandleModal
|
|
appInfo={currentApp}
|
|
visible={showModal}
|
|
type={modalType}
|
|
onChangeVisible={(value => setShowModal(value))}
|
|
onClose={() => setShowModal(false)}
|
|
onRefresh={onRefresh}
|
|
></AppHandleModal>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SideBar; |