- 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>
171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
import { useState } from 'react';
|
||
import { Table, Input, Button, Tag, Space } from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import { CloseOutlined, FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
|
||
import { useSchemaStore } from '../../store/schemaStore.ts';
|
||
import { STATUS_COLORS, STATUS_LABELS } from '../../constants/statusColors.ts';
|
||
import { mockData } from '../../mock/schemaData.ts';
|
||
import type { EntityStatus, Line } from '../../types/index.ts';
|
||
|
||
const { Search } = Input;
|
||
|
||
interface ConnectionRow {
|
||
key: string;
|
||
lineName: string;
|
||
lineStatus: EntityStatus;
|
||
deviceAName: string;
|
||
portAName: string;
|
||
deviceZName: string;
|
||
portZName: string;
|
||
}
|
||
|
||
export function ConnectionsPanel() {
|
||
const visible = useSchemaStore((s) => s.connectionsPanelVisible);
|
||
const setVisible = useSchemaStore((s) => s.setConnectionsPanelVisible);
|
||
const panelData = useSchemaStore((s) => s.connectionsPanelData);
|
||
const [searchValue, setSearchValue] = useState('');
|
||
const [expanded, setExpanded] = useState(false);
|
||
|
||
if (!visible || !panelData) return null;
|
||
|
||
// Build rows from panel data
|
||
const rows: ConnectionRow[] = [];
|
||
|
||
if (panelData.line) {
|
||
// Single line mode
|
||
const line = panelData.line as Line;
|
||
const portA = panelData.portA as { name: string } | null;
|
||
const portZ = panelData.portZ as { name: string } | null;
|
||
const devA = panelData.deviceA as { name: string } | null;
|
||
const devZ = panelData.deviceZ as { name: string } | null;
|
||
rows.push({
|
||
key: line.id,
|
||
lineName: line.name,
|
||
lineStatus: line.status,
|
||
deviceAName: devA?.name ?? '—',
|
||
portAName: portA?.name ?? '—',
|
||
deviceZName: devZ?.name ?? '—',
|
||
portZName: portZ?.name ?? '—',
|
||
});
|
||
} else if (panelData.lines) {
|
||
// Multiple lines mode
|
||
const lines = panelData.lines as Line[];
|
||
for (const line of lines) {
|
||
const portA = mockData.ports.find((p) => p.id === line.portAId);
|
||
const portZ = mockData.ports.find((p) => p.id === line.portZId);
|
||
const devA = portA ? mockData.devices.find((d) => d.id === portA.deviceId) : null;
|
||
const devZ = portZ ? mockData.devices.find((d) => d.id === portZ.deviceId) : null;
|
||
rows.push({
|
||
key: line.id,
|
||
lineName: line.name,
|
||
lineStatus: line.status,
|
||
deviceAName: devA?.name ?? '—',
|
||
portAName: portA?.name ?? '—',
|
||
deviceZName: devZ?.name ?? '—',
|
||
portZName: portZ?.name ?? '—',
|
||
});
|
||
}
|
||
}
|
||
|
||
const filtered = searchValue
|
||
? rows.filter(
|
||
(r) =>
|
||
r.lineName.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||
r.deviceAName.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||
r.deviceZName.toLowerCase().includes(searchValue.toLowerCase()),
|
||
)
|
||
: rows;
|
||
|
||
const columns: ColumnsType<ConnectionRow> = [
|
||
{
|
||
title: 'Линия',
|
||
dataIndex: 'lineName',
|
||
key: 'lineName',
|
||
render: (name: string, record: ConnectionRow) => {
|
||
const colors = STATUS_COLORS[record.lineStatus];
|
||
return (
|
||
<Space size={4}>
|
||
<Tag color={colors.border} style={{ color: colors.text, fontSize: 10 }}>
|
||
{STATUS_LABELS[record.lineStatus]}
|
||
</Tag>
|
||
<span>{name}</span>
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: 'Устройство A',
|
||
key: 'deviceA',
|
||
render: (_: unknown, record: ConnectionRow) => (
|
||
<span>
|
||
{record.deviceAName} ({record.portAName})
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
title: 'Устройство Z',
|
||
key: 'deviceZ',
|
||
render: (_: unknown, record: ConnectionRow) => (
|
||
<span>
|
||
{record.deviceZName} ({record.portZName})
|
||
</span>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
height: expanded ? '60%' : '30%',
|
||
minHeight: 150,
|
||
borderTop: '1px solid #f0f0f0',
|
||
background: '#fff',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: '6px 12px',
|
||
borderBottom: '1px solid #f0f0f0',
|
||
}}
|
||
>
|
||
<span style={{ fontWeight: 600, fontSize: 13 }}>Соединения</span>
|
||
<Space size={4}>
|
||
<Search
|
||
placeholder="Поиск..."
|
||
size="small"
|
||
style={{ width: 180 }}
|
||
value={searchValue}
|
||
onChange={(e) => setSearchValue(e.target.value)}
|
||
allowClear
|
||
/>
|
||
<Button
|
||
size="small"
|
||
icon={expanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||
onClick={() => setExpanded(!expanded)}
|
||
/>
|
||
<Button
|
||
size="small"
|
||
icon={<CloseOutlined />}
|
||
onClick={() => setVisible(false)}
|
||
/>
|
||
</Space>
|
||
</div>
|
||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||
<Table
|
||
dataSource={filtered}
|
||
columns={columns}
|
||
size="small"
|
||
pagination={false}
|
||
style={{ fontSize: 11 }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|