feat: frontend MVP — детальная схема связей устройств (AntV X6)

- 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>
This commit is contained in:
Alina
2026-02-17 22:02:25 +03:00
commit ef816cdcf4
48 changed files with 8738 additions and 0 deletions

View File

@ -0,0 +1,176 @@
import { Button, Slider, Space, Switch, Tooltip, message } from 'antd';
import {
ZoomInOutlined,
ZoomOutOutlined,
ExpandOutlined,
PlusOutlined,
DeleteOutlined,
ReloadOutlined,
PictureOutlined,
AppstoreOutlined,
NodeIndexOutlined,
EyeOutlined,
EditOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import { useSchemaStore } from '../../store/schemaStore.ts';
export function Toolbar() {
const graph = useSchemaStore((s) => s.graph);
const mode = useSchemaStore((s) => s.mode);
const setMode = useSchemaStore((s) => s.setMode);
const displaySettings = useSchemaStore((s) => s.displaySettings);
const toggleGrid = useSchemaStore((s) => s.toggleGrid);
const toggleMinimap = useSchemaStore((s) => s.toggleMinimap);
const switchLineType = useSchemaStore((s) => s.switchLineType);
const toggleLabels = useSchemaStore((s) => s.toggleLabels);
const setLegendVisible = useSchemaStore((s) => s.setLegendVisible);
const zoom = graph ? Math.round(graph.zoom() * 100) : 100;
const handleZoomIn = () => graph?.zoom(0.1);
const handleZoomOut = () => graph?.zoom(-0.1);
const handleFit = () => graph?.zoomToFit({ padding: 40 });
const handleZoomChange = (value: number) => {
if (graph) {
graph.zoomTo(value / 100);
}
};
const handleExportPng = () => {
message.info('В разработке');
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 16px',
height: 48,
borderBottom: '1px solid #f0f0f0',
background: '#fff',
flexShrink: 0,
}}
>
{/* Left: display settings */}
<Space size="middle">
<Tooltip title="Сетка">
<Switch
size="small"
checked={displaySettings.showGrid}
onChange={toggleGrid}
checkedChildren={<AppstoreOutlined />}
unCheckedChildren={<AppstoreOutlined />}
/>
</Tooltip>
<Tooltip title="Мини-карта">
<Switch
size="small"
checked={displaySettings.showMinimap}
onChange={toggleMinimap}
/>
</Tooltip>
<Tooltip
title={
displaySettings.lineType === 'manhattan'
? 'Ломаные линии'
: 'Прямые линии'
}
>
<Button
size="small"
icon={<NodeIndexOutlined />}
onClick={switchLineType}
type={displaySettings.lineType === 'manhattan' ? 'primary' : 'default'}
/>
</Tooltip>
<Tooltip title="Подписи">
<Switch
size="small"
checked={displaySettings.showLabels}
onChange={toggleLabels}
/>
</Tooltip>
<Tooltip title="Легенда">
<Button
size="small"
icon={<InfoCircleOutlined />}
onClick={() => setLegendVisible(true)}
/>
</Tooltip>
</Space>
{/* Center: actions */}
<Space>
<Tooltip title="Добавить объект">
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => message.info('В разработке')}
/>
</Tooltip>
<Tooltip title="Удалить">
<Button
size="small"
icon={<DeleteOutlined />}
onClick={() => {
if (graph) {
const cells = graph.getSelectedCells();
if (cells.length) graph.removeCells(cells);
}
}}
/>
</Tooltip>
<Tooltip title="Обновить раскладку">
<Button
size="small"
icon={<ReloadOutlined />}
onClick={() => message.info('В разработке')}
/>
</Tooltip>
<Tooltip title="Экспорт PNG">
<Button
size="small"
icon={<PictureOutlined />}
onClick={handleExportPng}
/>
</Tooltip>
</Space>
{/* Right: zoom + mode */}
<Space size="middle">
<Space size={4}>
<Tooltip title="Уменьшить">
<Button size="small" icon={<ZoomOutOutlined />} onClick={handleZoomOut} />
</Tooltip>
<Slider
style={{ width: 100 }}
min={10}
max={300}
value={zoom}
onChange={handleZoomChange}
tooltip={{ formatter: (v) => `${v}%` }}
/>
<Tooltip title="Увеличить">
<Button size="small" icon={<ZoomInOutlined />} onClick={handleZoomIn} />
</Tooltip>
<Tooltip title="Уместить на экран">
<Button size="small" icon={<ExpandOutlined />} onClick={handleFit} />
</Tooltip>
</Space>
<Tooltip title={mode === 'view' ? 'Режим просмотра' : 'Режим редактирования'}>
<Button
size="small"
type={mode === 'edit' ? 'primary' : 'default'}
icon={mode === 'view' ? <EyeOutlined /> : <EditOutlined />}
onClick={() => setMode(mode === 'view' ? 'edit' : 'view')}
>
{mode === 'view' ? 'Просмотр' : 'Редактирование'}
</Button>
</Tooltip>
</Space>
</div>
);
}