feat: редизайн UI — приглушённые цвета, шапки устройств, компактные контейнеры
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
- Цвета статусов: приглушённые тона для печати, красный акцент для неисправных - DeviceNode: название в цветной шапке, карты ниже без наложений - Перенос слов вместо обрезки во всех нодах - Лейаут: убран лишний gap после последнего слоя, SITE_MIN_WIDTH 250, корректный расчёт startY от шапки сайта - Белый фон графа, чёрные линии по умолчанию Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -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;
|
||||
|
||||
@ -8,39 +8,39 @@ export interface StatusColorSet {
|
||||
|
||||
export const STATUS_COLORS: Record<EntityStatus, StatusColorSet> = {
|
||||
[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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<string, { x: number; y: number; width: number; height: number }>;
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,51 +30,73 @@ 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',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: DEVICE_HEADER_HEIGHT,
|
||||
minHeight: DEVICE_HEADER_HEIGHT,
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
fontSize: 11,
|
||||
marginBottom: 2,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: '13px',
|
||||
}}
|
||||
>
|
||||
{data.name}
|
||||
</div>
|
||||
{isActive ? (
|
||||
<>
|
||||
{data.networkName && (
|
||||
<div style={{ color: '#595959', fontSize: 9 }}>{data.networkName}</div>
|
||||
data.networkName && (
|
||||
<div style={{ opacity: 0.85, fontSize: 9 }}>{data.networkName}</div>
|
||||
)
|
||||
) : (
|
||||
data.marking && (
|
||||
<div style={{ opacity: 0.85, fontSize: 9 }}>{data.marking}</div>
|
||||
)
|
||||
)}
|
||||
{data.ipAddress && (
|
||||
<div style={{ color: '#1890ff', fontSize: 9 }}>{data.ipAddress}</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '2px 6px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{isActive ? (
|
||||
data.ipAddress && (
|
||||
<div style={{ color: '#555555', fontSize: 9 }}>{data.ipAddress}</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{data.marking && (
|
||||
<div style={{ color: '#595959', fontSize: 9 }}>{data.marking}</div>
|
||||
)}
|
||||
{data.id1 && (
|
||||
<div style={{ color: '#8c8c8c', fontSize: 9 }}>{data.id1}</div>
|
||||
<div style={{ color: '#666666', fontSize: 9 }}>{data.id1}</div>
|
||||
)}
|
||||
{data.id2 && (
|
||||
<div style={{ color: '#8c8c8c', fontSize: 9 }}>{data.id2}</div>
|
||||
<div style={{ color: '#888888', fontSize: 9 }}>{data.id2}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 700, fontSize: 12, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 11, wordBreak: 'break-word' }}>
|
||||
{data.name}
|
||||
</div>
|
||||
<div style={{ opacity: 0.8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{data.address}
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 10 }}>
|
||||
ERP: {data.erpCode} | 1С: {data.code1C}
|
||||
<div style={{ opacity: 0.8, fontSize: 9, wordBreak: 'break-word' }}>
|
||||
{data.address} | ERP: {data.erpCode} | 1С: {data.code1C}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user