- React 18 + TypeScript strict + AntV X6 2.x + AntD 5 + Zustand - Custom nodes: SiteNode, CrossDeviceNode, SpliceNode, DeviceNode, CardNode - 8-слойный автолейаут, порты (left/right), линии с цветами по статусу - Toolbar, дерево навигации, карточка объекта, таблица соединений - Контекстные меню, легенда, drag линий/нод, создание линий из портов - Моковые данные: 3 сайта, 10 устройств, 15 линий Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
4.1 KiB
TypeScript
150 lines
4.1 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { Tree, Input } from 'antd';
|
|
import type { TreeDataNode } from 'antd';
|
|
import {
|
|
ApartmentOutlined,
|
|
HddOutlined,
|
|
ClusterOutlined,
|
|
} from '@ant-design/icons';
|
|
import { mockData } from '../../mock/schemaData.ts';
|
|
import { useSchemaStore } from '../../store/schemaStore.ts';
|
|
|
|
const { Search } = Input;
|
|
|
|
export function LeftPanel() {
|
|
const [searchValue, setSearchValue] = useState('');
|
|
const [expandedKeys, setExpandedKeys] = useState<string[]>(['sites', 'all-devices']);
|
|
const graph = useSchemaStore((s) => s.graph);
|
|
const setRightPanelData = useSchemaStore((s) => s.setRightPanelData);
|
|
|
|
const treeData = useMemo((): TreeDataNode[] => {
|
|
const sitesTree: TreeDataNode[] = mockData.sites
|
|
.filter((s) => !s.parentSiteId)
|
|
.map((site) => {
|
|
const children: TreeDataNode[] = [];
|
|
|
|
// Add devices belonging to this site
|
|
const siteDevices = mockData.devices.filter(
|
|
(d) => d.siteId === site.id,
|
|
);
|
|
for (const device of siteDevices) {
|
|
children.push({
|
|
key: device.id,
|
|
title: device.name,
|
|
icon: <HddOutlined />,
|
|
});
|
|
}
|
|
|
|
// Add child sites
|
|
const childSites = mockData.sites.filter(
|
|
(s) => s.parentSiteId === site.id,
|
|
);
|
|
for (const childSite of childSites) {
|
|
const childDevices = mockData.devices.filter(
|
|
(d) => d.siteId === childSite.id,
|
|
);
|
|
children.push({
|
|
key: childSite.id,
|
|
title: childSite.name,
|
|
icon: <ApartmentOutlined />,
|
|
children: childDevices.map((d) => ({
|
|
key: d.id,
|
|
title: d.name,
|
|
icon: <HddOutlined />,
|
|
})),
|
|
});
|
|
}
|
|
|
|
return {
|
|
key: site.id,
|
|
title: site.name,
|
|
icon: <ApartmentOutlined />,
|
|
children,
|
|
};
|
|
});
|
|
|
|
return [
|
|
{
|
|
key: 'sites',
|
|
title: 'Сайты',
|
|
icon: <ClusterOutlined />,
|
|
children: sitesTree,
|
|
},
|
|
];
|
|
}, []);
|
|
|
|
const filteredTreeData = useMemo(() => {
|
|
if (!searchValue) return treeData;
|
|
|
|
const filterTree = (nodes: TreeDataNode[]): TreeDataNode[] => {
|
|
return nodes
|
|
.map((node) => {
|
|
const title = String(node.title ?? '');
|
|
const match = title.toLowerCase().includes(searchValue.toLowerCase());
|
|
const filteredChildren = node.children
|
|
? filterTree(node.children)
|
|
: [];
|
|
if (match || filteredChildren.length > 0) {
|
|
return { ...node, children: filteredChildren };
|
|
}
|
|
return null;
|
|
})
|
|
.filter(Boolean) as TreeDataNode[];
|
|
};
|
|
|
|
return filterTree(treeData);
|
|
}, [treeData, searchValue]);
|
|
|
|
const handleSelect = (selectedKeys: React.Key[]) => {
|
|
const key = selectedKeys[0] as string;
|
|
if (!key || !graph) return;
|
|
|
|
// Find the node on the graph and center on it
|
|
const cell = graph.getCellById(key);
|
|
if (cell) {
|
|
graph.centerCell(cell);
|
|
graph.select(cell);
|
|
|
|
// Set right panel data
|
|
const data = cell.getData() as Record<string, unknown> | undefined;
|
|
if (data) {
|
|
setRightPanelData(data);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
background: '#fff',
|
|
borderRight: '1px solid #f0f0f0',
|
|
}}
|
|
>
|
|
<div style={{ padding: '8px 12px', borderBottom: '1px solid #f0f0f0' }}>
|
|
<Search
|
|
placeholder="Поиск..."
|
|
size="small"
|
|
value={searchValue}
|
|
onChange={(e) => setSearchValue(e.target.value)}
|
|
allowClear
|
|
/>
|
|
</div>
|
|
<div style={{ flex: 1, overflow: 'auto', padding: '4px 0' }}>
|
|
<Tree
|
|
showIcon
|
|
treeData={filteredTreeData}
|
|
expandedKeys={expandedKeys}
|
|
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
|
onSelect={handleSelect}
|
|
blockNode
|
|
style={{ fontSize: 12 }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|