feat: миграция frontend React 18 → Angular 19
All checks were successful
continuous-integration/drone/push Build is passing

Полная миграция фронтенда на Angular 19 + PrimeNG + NgRx SignalStore.

- React 18 + AntD 5 + Zustand + Vite → Angular 19 + PrimeNG 19 + NgRx SignalStore + Angular CLI
- Ноды X6: React-компоненты → чистые DOM-функции через Shape.HTML.register с effect: ['data']
- Все 5 типов нод (site, device, cross-device, splice, card) переписаны как рендереры
- Zustand store → NgRx signalStore (schema.store.ts)
- AntD компоненты → PrimeNG (p-tree, p-table, p-menu, p-toggleSwitch, p-slider, p-button)
- 13 файлов чистого TypeScript переиспользованы as-is (типы, константы, утилиты, мок, layout, ports, edges)
- Структура файлов переименована в kebab-case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alina
2026-02-18 08:52:31 +03:00
parent 323410ead7
commit f355caa9ad
66 changed files with 15508 additions and 4324 deletions

View File

@ -0,0 +1,42 @@
import type { Cell } from '@antv/x6';
import { STATUS_COLORS } from '../../../constants/status-colors';
import type { EntityStatus } from '../../../types/index';
interface CardNodeData {
slotName: string;
networkName: string;
status: EntityStatus;
}
export function renderCardNode(cell: Cell): HTMLElement {
const data = cell.getData() as CardNodeData;
const colors = STATUS_COLORS[data.status];
const size = (cell as any).getSize() as { width: number; height: number };
const container = document.createElement('div');
Object.assign(container.style, {
width: `${size.width}px`,
height: `${size.height}px`,
borderTop: `1.5px solid ${colors.border}`,
borderBottom: `1.5px solid ${colors.border}`,
borderLeft: `5px solid ${colors.border}`,
borderRight: `5px solid ${colors.border}`,
borderRadius: '0',
background: colors.fill,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
fontSize: '9px',
fontWeight: '600',
color: colors.text,
textAlign: 'center',
wordBreak: 'break-word',
lineHeight: '12px',
padding: '2px 4px',
});
container.textContent = `${data.slotName}:${data.networkName}`;
return container;
}

View File

@ -0,0 +1,110 @@
import type { Cell } from '@antv/x6';
import { STATUS_COLORS } from '../../../constants/status-colors';
import { CROSS_BORDER_RADIUS } from '../../../constants/sizes';
import type { EntityStatus } from '../../../types/index';
interface CrossDeviceData {
name: string;
networkName: string;
marking: string;
id1: string;
id2: string;
status: EntityStatus;
}
export function renderCrossDeviceNode(cell: Cell): HTMLElement {
const data = cell.getData() as CrossDeviceData;
const colors = STATUS_COLORS[data.status];
const size = (cell as any).getSize() as { width: number; height: number };
const r = CROSS_BORDER_RADIUS;
const w = size.width;
const h = size.height;
// Asymmetric rounding: top-left and bottom-right rounded
const path = `
M ${r} 0
L ${w} 0
L ${w} ${h - r}
Q ${w} ${h} ${w - r} ${h}
L 0 ${h}
L 0 ${r}
Q 0 0 ${r} 0
Z
`;
const container = document.createElement('div');
Object.assign(container.style, {
width: `${w}px`,
height: `${h}px`,
position: 'relative',
});
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', String(w));
svg.setAttribute('height', String(h));
Object.assign(svg.style, { position: 'absolute', top: '0', left: '0' });
const pathEl = document.createElementNS(svgNS, 'path');
pathEl.setAttribute('d', path);
pathEl.setAttribute('fill', colors.fill);
pathEl.setAttribute('stroke', colors.border);
pathEl.setAttribute('stroke-width', '1');
svg.appendChild(pathEl);
container.appendChild(svg);
// Text overlay
const textDiv = document.createElement('div');
Object.assign(textDiv.style, {
position: 'absolute',
top: '0',
left: '0',
width: `${w}px`,
height: `${h}px`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '8px 6px',
boxSizing: 'border-box',
textAlign: 'center',
fontSize: '10px',
lineHeight: '14px',
pointerEvents: 'none',
});
const nameEl = document.createElement('div');
Object.assign(nameEl.style, {
fontWeight: '700',
fontSize: '11px',
marginBottom: '4px',
wordBreak: 'break-word',
});
nameEl.textContent = data.name;
textDiv.appendChild(nameEl);
if (data.networkName) {
const el = document.createElement('div');
Object.assign(el.style, { color: '#595959', fontSize: '9px' });
el.textContent = data.networkName;
textDiv.appendChild(el);
}
if (data.marking) {
const el = document.createElement('div');
Object.assign(el.style, { color: '#8c8c8c', fontSize: '9px' });
el.textContent = data.marking;
textDiv.appendChild(el);
}
if (data.id1) {
const el = document.createElement('div');
Object.assign(el.style, { color: '#8c8c8c', fontSize: '9px' });
el.textContent = data.id1;
textDiv.appendChild(el);
}
container.appendChild(textDiv);
return container;
}

View File

@ -0,0 +1,119 @@
import type { Cell } from '@antv/x6';
import { STATUS_COLORS } from '../../../constants/status-colors';
import { DEVICE_BORDER_RADIUS, DEVICE_HEADER_HEIGHT } from '../../../constants/sizes';
import { DeviceGroup, type EntityStatus } from '../../../types/index';
interface DeviceNodeData {
name: string;
networkName: string;
ipAddress: string;
marking: string;
id1: string;
id2: string;
group: DeviceGroup;
status: EntityStatus;
}
export function renderDeviceNode(cell: Cell): HTMLElement {
const data = cell.getData() as DeviceNodeData;
const colors = STATUS_COLORS[data.status];
const size = (cell as any).getSize() as { width: number; height: number };
const isActive = data.group === DeviceGroup.Active;
const container = document.createElement('div');
Object.assign(container.style, {
width: `${size.width}px`,
height: `${size.height}px`,
border: `1px solid ${colors.border}`,
borderRadius: `${DEVICE_BORDER_RADIUS}px`,
background: colors.fill,
display: 'flex',
flexDirection: 'column',
boxSizing: 'border-box',
fontSize: '10px',
lineHeight: '14px',
overflow: 'hidden',
});
// Header
const header = document.createElement('div');
Object.assign(header.style, {
height: `${DEVICE_HEADER_HEIGHT}px`,
minHeight: `${DEVICE_HEADER_HEIGHT}px`,
background: colors.border,
color: '#ffffff',
borderRadius: `${DEVICE_BORDER_RADIUS - 1}px ${DEVICE_BORDER_RADIUS - 1}px 0 0`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '2px 6px',
textAlign: 'center',
});
const nameEl = document.createElement('div');
Object.assign(nameEl.style, {
fontWeight: '700',
fontSize: '11px',
wordBreak: 'break-word',
lineHeight: '13px',
});
nameEl.textContent = data.name;
header.appendChild(nameEl);
if (isActive) {
if (data.networkName) {
const subEl = document.createElement('div');
Object.assign(subEl.style, { opacity: '0.85', fontSize: '9px' });
subEl.textContent = data.networkName;
header.appendChild(subEl);
}
} else {
if (data.marking) {
const subEl = document.createElement('div');
Object.assign(subEl.style, { opacity: '0.85', fontSize: '9px' });
subEl.textContent = data.marking;
header.appendChild(subEl);
}
}
container.appendChild(header);
// Body
const body = document.createElement('div');
Object.assign(body.style, {
flex: '1',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '2px 6px',
textAlign: 'center',
});
if (isActive) {
if (data.ipAddress) {
const ipEl = document.createElement('div');
Object.assign(ipEl.style, { color: '#555555', fontSize: '9px' });
ipEl.textContent = data.ipAddress;
body.appendChild(ipEl);
}
} else {
if (data.id1) {
const id1El = document.createElement('div');
Object.assign(id1El.style, { color: '#666666', fontSize: '9px' });
id1El.textContent = data.id1;
body.appendChild(id1El);
}
if (data.id2) {
const id2El = document.createElement('div');
Object.assign(id2El.style, { color: '#888888', fontSize: '9px' });
id2El.textContent = data.id2;
body.appendChild(id2El);
}
}
container.appendChild(body);
return container;
}

View File

@ -0,0 +1,68 @@
import type { Cell } from '@antv/x6';
import { STATUS_COLORS } from '../../../constants/status-colors';
import { SITE_HEADER_HEIGHT } from '../../../constants/sizes';
import type { EntityStatus } from '../../../types/index';
interface SiteNodeData {
name: string;
address: string;
erpCode: string;
code1C: string;
status: EntityStatus;
}
export function renderSiteNode(cell: Cell): HTMLElement {
const data = cell.getData() as SiteNodeData;
const colors = STATUS_COLORS[data.status];
const size = (cell as any).getSize() as { width: number; height: number };
const container = document.createElement('div');
Object.assign(container.style, {
width: `${size.width}px`,
height: `${size.height}px`,
border: `3.87px solid ${colors.border}`,
borderRadius: '0',
background: 'transparent',
position: 'relative',
boxSizing: 'border-box',
overflow: 'visible',
pointerEvents: 'none',
});
const header = document.createElement('div');
Object.assign(header.style, {
height: `${SITE_HEADER_HEIGHT}px`,
background: '#1a1a2e',
color: '#ffffff',
padding: '4px 10px',
fontSize: '10px',
lineHeight: '14px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: '1px',
pointerEvents: 'auto',
});
const nameEl = document.createElement('div');
Object.assign(nameEl.style, {
fontWeight: '700',
fontSize: '11px',
wordBreak: 'break-word',
});
nameEl.textContent = data.name;
const infoEl = document.createElement('div');
Object.assign(infoEl.style, {
opacity: '0.8',
fontSize: '9px',
wordBreak: 'break-word',
});
infoEl.textContent = `${data.address} | ERP: ${data.erpCode} | 1С: ${data.code1C}`;
header.appendChild(nameEl);
header.appendChild(infoEl);
container.appendChild(header);
return container;
}

View File

@ -0,0 +1,55 @@
import type { Cell } from '@antv/x6';
import { STATUS_COLORS } from '../../../constants/status-colors';
import { SPLICE_BORDER_RADIUS } from '../../../constants/sizes';
import type { EntityStatus } from '../../../types/index';
interface SpliceNodeData {
name: string;
marking: string;
id1: string;
id2: string;
status: EntityStatus;
}
export function renderSpliceNode(cell: Cell): HTMLElement {
const data = cell.getData() as SpliceNodeData;
const colors = STATUS_COLORS[data.status];
const size = (cell as any).getSize() as { width: number; height: number };
const container = document.createElement('div');
Object.assign(container.style, {
width: `${size.width}px`,
height: `${size.height}px`,
border: `1px solid ${colors.border}`,
borderRadius: `${SPLICE_BORDER_RADIUS}px`,
background: colors.fill,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
textAlign: 'center',
padding: '4px',
fontSize: '10px',
lineHeight: '14px',
});
const nameEl = document.createElement('div');
Object.assign(nameEl.style, {
fontWeight: '700',
fontSize: '11px',
marginBottom: '2px',
wordBreak: 'break-word',
});
nameEl.textContent = data.name;
container.appendChild(nameEl);
if (data.marking) {
const markingEl = document.createElement('div');
Object.assign(markingEl.style, { color: '#595959', fontSize: '9px' });
markingEl.textContent = data.marking;
container.appendChild(markingEl);
}
return container;
}