feat: миграция frontend React 18 → Angular 19
All checks were successful
continuous-integration/drone/push Build is passing
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:
42
frontend/src/app/features/schema/nodes/card-node.renderer.ts
Normal file
42
frontend/src/app/features/schema/nodes/card-node.renderer.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
119
frontend/src/app/features/schema/nodes/device-node.renderer.ts
Normal file
119
frontend/src/app/features/schema/nodes/device-node.renderer.ts
Normal 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;
|
||||
}
|
||||
68
frontend/src/app/features/schema/nodes/site-node.renderer.ts
Normal file
68
frontend/src/app/features/schema/nodes/site-node.renderer.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user