From fd6373bcb08ee22e7bc9e43f79d0fd16c3b17f79 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 17 Feb 2026 22:52:26 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D0=B9=D0=BD=20UI=20=E2=80=94=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB?= =?UTF-8?q?=D1=83=D1=88=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=86=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=B0,=20=D1=88=D0=B0=D0=BF=D0=BA=D0=B8=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2,=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=B0=D0=BA=D1=82=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Цвета статусов: приглушённые тона для печати, красный акцент для неисправных - DeviceNode: название в цветной шапке, карты ниже без наложений - Перенос слов вместо обрезки во всех нодах - Лейаут: убран лишний gap после последнего слоя, SITE_MIN_WIDTH 250, корректный расчёт startY от шапки сайта - Белый фон графа, чёрные линии по умолчанию Co-Authored-By: Claude Opus 4.6 --- frontend/src/constants/sizes.ts | 13 +-- frontend/src/constants/statusColors.ts | 40 ++++---- .../src/features/schema/graph/initGraph.ts | 8 +- .../src/features/schema/helpers/dataMapper.ts | 11 ++- .../src/features/schema/layout/autoLayout.ts | 45 +++++---- .../src/features/schema/nodes/CardNode.tsx | 7 +- .../src/features/schema/nodes/DeviceNode.tsx | 92 ++++++++++++------- .../src/features/schema/nodes/SiteNode.tsx | 15 ++- 8 files changed, 134 insertions(+), 97 deletions(-) diff --git a/frontend/src/constants/sizes.ts b/frontend/src/constants/sizes.ts index 23adc1d..d7f97bc 100644 --- a/frontend/src/constants/sizes.ts +++ b/frontend/src/constants/sizes.ts @@ -1,6 +1,6 @@ -export const SITE_HEADER_HEIGHT = 80; -export const SITE_PADDING = 50; -export const SITE_MIN_WIDTH = 500; +export const SITE_HEADER_HEIGHT = 56; +export const SITE_PADDING = 30; +export const SITE_MIN_WIDTH = 250; export const SITE_MIN_HEIGHT = 200; export const CROSS_WIDTH = 115; @@ -12,6 +12,7 @@ export const SPLICE_BORDER_RADIUS = 6; export const DEVICE_MIN_WIDTH = 140; export const DEVICE_MIN_HEIGHT = 80; +export const DEVICE_HEADER_HEIGHT = 38; export const DEVICE_BORDER_RADIUS = 6; export const CARD_WIDTH = 100; @@ -19,6 +20,6 @@ export const CARD_HEIGHT = 40; export const PORT_RADIUS = 6; -export const LAYER_GAP = 80; -export const DEVICE_GAP = 60; -export const LAYER_PADDING_X = 40; +export const LAYER_GAP = 60; +export const DEVICE_GAP = 100; +export const LAYER_PADDING_X = 60; diff --git a/frontend/src/constants/statusColors.ts b/frontend/src/constants/statusColors.ts index a6468ef..cb6b70f 100644 --- a/frontend/src/constants/statusColors.ts +++ b/frontend/src/constants/statusColors.ts @@ -8,39 +8,39 @@ export interface StatusColorSet { export const STATUS_COLORS: Record = { [EntityStatus.Active]: { - border: '#52c41a', - fill: '#f6ffed', - text: '#389e0d', + border: '#2d7a3a', + fill: '#f4f9f4', + text: '#2d7a3a', }, [EntityStatus.Planned]: { - border: '#1890ff', - fill: '#e6f7ff', - text: '#096dd9', + border: '#3568a8', + fill: '#f0f5fb', + text: '#3568a8', }, [EntityStatus.UnderConstruction]: { - border: '#faad14', - fill: '#fffbe6', - text: '#d48806', + border: '#9a7520', + fill: '#faf6ed', + text: '#7a5d18', }, [EntityStatus.Reserved]: { - border: '#722ed1', - fill: '#f9f0ff', - text: '#531dab', + border: '#6b5b95', + fill: '#f5f3f9', + text: '#6b5b95', }, [EntityStatus.Faulty]: { - border: '#ff4d4f', - fill: '#fff2f0', - text: '#cf1322', + border: '#cc0000', + fill: '#fef0f0', + text: '#cc0000', }, [EntityStatus.Decommissioned]: { - border: '#8c8c8c', - fill: '#fafafa', - text: '#595959', + border: '#aaaaaa', + fill: '#f5f5f5', + text: '#888888', }, [EntityStatus.Unknown]: { - border: '#bfbfbf', + border: '#c0c0c0', fill: '#fafafa', - text: '#8c8c8c', + text: '#999999', }, }; diff --git a/frontend/src/features/schema/graph/initGraph.ts b/frontend/src/features/schema/graph/initGraph.ts index 1653f50..0037bb0 100644 --- a/frontend/src/features/schema/graph/initGraph.ts +++ b/frontend/src/features/schema/graph/initGraph.ts @@ -14,12 +14,12 @@ export function initGraph( const graph = new Graph({ container, autoResize: true, - background: { color: '#f8f9fa' }, + background: { color: '#ffffff' }, grid: { visible: true, size: 10, type: 'dot', - args: { color: '#d9d9d9', thickness: 1 }, + args: { color: '#e0e0e0', thickness: 1 }, }, panning: { enabled: true, eventTypes: ['rightMouseDown'] }, mousewheel: { @@ -47,7 +47,7 @@ export function initGraph( return this.createEdge({ attrs: { line: { - stroke: '#52c41a', + stroke: '#2d7a3a', strokeWidth: 2, targetMarker: null, sourceMarker: null, @@ -87,7 +87,7 @@ export function initGraph( highlighting: { magnetAvailable: { name: 'stroke', - args: { attrs: { fill: '#5F95FF', stroke: '#5F95FF' } }, + args: { attrs: { fill: '#333333', stroke: '#333333' } }, }, }, }); diff --git a/frontend/src/features/schema/helpers/dataMapper.ts b/frontend/src/features/schema/helpers/dataMapper.ts index d500ad6..87df57a 100644 --- a/frontend/src/features/schema/helpers/dataMapper.ts +++ b/frontend/src/features/schema/helpers/dataMapper.ts @@ -11,6 +11,7 @@ import { SPLICE_SIZE, DEVICE_MIN_WIDTH, DEVICE_MIN_HEIGHT, + DEVICE_HEADER_HEIGHT, CARD_WIDTH, CARD_HEIGHT, } from '../../../constants/sizes.ts'; @@ -36,6 +37,7 @@ function getDeviceSize( deviceName: string, marking: string, portCount: number, + cardCount: number, ): { width: number; height: number } { if ( category === DeviceCategory.CrossOptical || @@ -50,9 +52,11 @@ function getDeviceSize( return { width: SPLICE_SIZE, height: SPLICE_SIZE }; } const portHeight = Math.max(portCount * 22, 60); + const cardsHeight = cardCount > 0 ? cardCount * (CARD_HEIGHT + 6) + 8 : 0; + const bodyHeight = Math.max(portHeight, cardsHeight); return { width: DEVICE_MIN_WIDTH, - height: Math.max(DEVICE_MIN_HEIGHT, portHeight + 30), + height: Math.max(DEVICE_MIN_HEIGHT, DEVICE_HEADER_HEIGHT + bodyHeight + 10), }; } @@ -105,7 +109,8 @@ export function buildGraphData( (p) => p.deviceId === device.id && !p.cardId, ); const shape = getDeviceShape(device.category, device.name, device.marking); - const size = getDeviceSize(device.category, device.name, device.marking, devicePorts.length); + const deviceCards = data.cards.filter((c) => c.deviceId === device.id && c.visible); + const size = getDeviceSize(device.category, device.name, device.marking, devicePorts.length, deviceCards.length); const portItems = devicePorts.map((port) => { const resolvedSide = portSideMap.get(port.id) ?? port.side; @@ -191,7 +196,7 @@ export function buildGraphData( ).indexOf(card); const cardX = parentPos.x + 10; - const cardY = parentPos.y + 40 + cardIndex * (CARD_HEIGHT + 6); + const cardY = parentPos.y + DEVICE_HEADER_HEIGHT + 8 + cardIndex * (CARD_HEIGHT + 6); const portItems = cardPorts.map((port) => { const resolvedSide = portSideMap.get(port.id) ?? port.side; diff --git a/frontend/src/features/schema/layout/autoLayout.ts b/frontend/src/features/schema/layout/autoLayout.ts index 695af77..386c283 100644 --- a/frontend/src/features/schema/layout/autoLayout.ts +++ b/frontend/src/features/schema/layout/autoLayout.ts @@ -7,6 +7,8 @@ import { SPLICE_SIZE, DEVICE_MIN_WIDTH, DEVICE_MIN_HEIGHT, + DEVICE_HEADER_HEIGHT, + CARD_HEIGHT, SITE_HEADER_HEIGHT, SITE_PADDING, SITE_MIN_WIDTH, @@ -20,7 +22,7 @@ export interface LayoutResult { sitePositions: Map; } -function getDeviceSize(device: Device, portCount: number): { width: number; height: number } { +function getDeviceSize(device: Device, portCount: number, cardCount: number): { width: number; height: number } { if ( device.category === DeviceCategory.CrossOptical || device.category === DeviceCategory.CrossCopper @@ -33,11 +35,13 @@ function getDeviceSize(device: Device, portCount: number): { width: number; heig return { width: SPLICE_SIZE, height: SPLICE_SIZE }; } - // Dynamic height based on port count + // Dynamic height based on port count + header + cards const portHeight = Math.max(portCount * 22, 60); + const cardsHeight = cardCount > 0 ? cardCount * (CARD_HEIGHT + 6) + 8 : 0; + const bodyHeight = Math.max(portHeight, cardsHeight); return { width: DEVICE_MIN_WIDTH, - height: Math.max(DEVICE_MIN_HEIGHT, portHeight + 30), + height: Math.max(DEVICE_MIN_HEIGHT, DEVICE_HEADER_HEIGHT + bodyHeight + 10), }; } @@ -67,7 +71,8 @@ function layoutDevicesInSite( for (const device of devices) { const layer = getLayerForDevice(device); const portCount = data.ports.filter((p) => p.deviceId === device.id && !p.cardId).length; - const size = getDeviceSize(device, portCount); + const cardCount = data.cards.filter((c) => c.deviceId === device.id && c.visible).length; + const size = getDeviceSize(device, portCount, cardCount); const existing = layers.get(layer); if (existing) { existing.push({ device, size }); @@ -91,7 +96,8 @@ function layoutDevicesInSite( let currentY = startY; let maxWidth = 0; - for (const [, layerDevices] of sortedLayers) { + for (let i = 0; i < sortedLayers.length; i++) { + const [, layerDevices] = sortedLayers[i]; let currentX = startX + LAYER_PADDING_X; let layerMaxHeight = 0; @@ -106,12 +112,17 @@ function layoutDevicesInSite( layerMaxHeight = Math.max(layerMaxHeight, size.height); } - maxWidth = Math.max(maxWidth, currentX - startX); - currentY += layerMaxHeight + LAYER_GAP; + // Subtract trailing DEVICE_GAP from width calculation + maxWidth = Math.max(maxWidth, currentX - DEVICE_GAP - startX + LAYER_PADDING_X); + currentY += layerMaxHeight; + // Add gap only between layers, not after the last one + if (i < sortedLayers.length - 1) { + currentY += LAYER_GAP; + } } const totalHeight = currentY - startY; - const totalWidth = Math.max(maxWidth + LAYER_PADDING_X, SITE_MIN_WIDTH); + const totalWidth = Math.max(maxWidth, SITE_MIN_WIDTH); return { positions, totalWidth, totalHeight }; } @@ -124,17 +135,19 @@ export function autoLayout(data: SchemaData): LayoutResult { const rootSites = data.sites.filter((s: Site) => !s.parentSiteId); const childSites = data.sites.filter((s: Site) => s.parentSiteId); + const siteStartY = 50; let siteX = 50; for (const site of rootSites) { const siteDevices = getSiteDevices(site.id, data); - const contentStartY = SITE_HEADER_HEIGHT + SITE_PADDING; + // Absolute Y where devices start: site top + header + padding + const devicesStartY = siteStartY + SITE_HEADER_HEIGHT + SITE_PADDING; const { positions, totalWidth, totalHeight } = layoutDevicesInSite( siteDevices, data, siteX + SITE_PADDING, - contentStartY + 50, // offset for site positioning + devicesStartY, ); // Add device positions @@ -149,13 +162,13 @@ export function autoLayout(data: SchemaData): LayoutResult { for (const childSite of siteChildren) { const childDevices = getSiteDevices(childSite.id, data); - const childContentStartY = contentStartY + totalHeight + 50; + const childSiteY = devicesStartY + totalHeight + SITE_PADDING; const childLayout = layoutDevicesInSite( childDevices, data, childX + SITE_PADDING, - childContentStartY + SITE_HEADER_HEIGHT + SITE_PADDING, + childSiteY + SITE_HEADER_HEIGHT + SITE_PADDING, ); for (const [deviceId, pos] of childLayout.positions.entries()) { @@ -167,7 +180,7 @@ export function autoLayout(data: SchemaData): LayoutResult { sitePositions.set(childSite.id, { x: childX, - y: childContentStartY, + y: childSiteY, width: childSiteWidth, height: childSiteHeight, }); @@ -177,16 +190,16 @@ export function autoLayout(data: SchemaData): LayoutResult { } const siteWidth = Math.max(totalWidth + SITE_PADDING * 2, childX - siteX); - const siteHeight = totalHeight + contentStartY + childSiteExtraHeight + SITE_PADDING; + const siteHeight = (devicesStartY - siteStartY) + totalHeight + childSiteExtraHeight + SITE_PADDING; sitePositions.set(site.id, { x: siteX, - y: 50, + y: siteStartY, width: siteWidth, height: siteHeight, }); - siteX += siteWidth + 150; + siteX += siteWidth + 120; } return { nodePositions, sitePositions }; diff --git a/frontend/src/features/schema/nodes/CardNode.tsx b/frontend/src/features/schema/nodes/CardNode.tsx index 2731ce7..0a70842 100644 --- a/frontend/src/features/schema/nodes/CardNode.tsx +++ b/frontend/src/features/schema/nodes/CardNode.tsx @@ -32,10 +32,9 @@ export function CardNode({ node }: { node: Node }) { fontWeight: 600, color: colors.text, textAlign: 'center', - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - padding: '0 4px', + wordBreak: 'break-word', + lineHeight: '12px', + padding: '2px 4px', }} > {data.slotName}:{data.networkName} diff --git a/frontend/src/features/schema/nodes/DeviceNode.tsx b/frontend/src/features/schema/nodes/DeviceNode.tsx index 77e33ba..bc86b64 100644 --- a/frontend/src/features/schema/nodes/DeviceNode.tsx +++ b/frontend/src/features/schema/nodes/DeviceNode.tsx @@ -1,6 +1,6 @@ import type { Node } from '@antv/x6'; import { STATUS_COLORS } from '../../../constants/statusColors.ts'; -import { DEVICE_BORDER_RADIUS } from '../../../constants/sizes.ts'; +import { DEVICE_BORDER_RADIUS, DEVICE_HEADER_HEIGHT } from '../../../constants/sizes.ts'; import { DeviceGroup, type EntityStatus } from '../../../types/index.ts'; interface DeviceNodeData { @@ -30,11 +30,7 @@ export function DeviceNode({ node }: { node: Node }) { background: colors.fill, display: 'flex', flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', boxSizing: 'border-box', - textAlign: 'center', - padding: '6px 8px', fontSize: 10, lineHeight: '14px', overflow: 'hidden', @@ -42,39 +38,65 @@ export function DeviceNode({ node }: { node: Node }) { >
- {data.name} +
+ {data.name} +
+ {isActive ? ( + data.networkName && ( +
{data.networkName}
+ ) + ) : ( + data.marking && ( +
{data.marking}
+ ) + )} +
+
+ {isActive ? ( + data.ipAddress && ( +
{data.ipAddress}
+ ) + ) : ( + <> + {data.id1 && ( +
{data.id1}
+ )} + {data.id2 && ( +
{data.id2}
+ )} + + )}
- {isActive ? ( - <> - {data.networkName && ( -
{data.networkName}
- )} - {data.ipAddress && ( -
{data.ipAddress}
- )} - - ) : ( - <> - {data.marking && ( -
{data.marking}
- )} - {data.id1 && ( -
{data.id1}
- )} - {data.id2 && ( -
{data.id2}
- )} - - )} ); } diff --git a/frontend/src/features/schema/nodes/SiteNode.tsx b/frontend/src/features/schema/nodes/SiteNode.tsx index eb8bf27..9cf716e 100644 --- a/frontend/src/features/schema/nodes/SiteNode.tsx +++ b/frontend/src/features/schema/nodes/SiteNode.tsx @@ -35,9 +35,9 @@ export function SiteNode({ node }: { node: Node }) { height: SITE_HEADER_HEIGHT, background: '#1a1a2e', color: '#ffffff', - padding: '6px 10px', - fontSize: 11, - lineHeight: '16px', + padding: '4px 10px', + fontSize: 10, + lineHeight: '14px', display: 'flex', flexDirection: 'column', justifyContent: 'center', @@ -45,14 +45,11 @@ export function SiteNode({ node }: { node: Node }) { pointerEvents: 'auto', }} > -
+
{data.name}
-
- {data.address} -
-
- ERP: {data.erpCode} | 1С: {data.code1C} +
+ {data.address} | ERP: {data.erpCode} | 1С: {data.code1C}