feat(NetworkMapUnifi): enhance label placement logic and update country flag rendering with CDN support
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s

This commit is contained in:
2026-02-18 14:05:20 +07:00
parent fdc39060a1
commit 1fda3a5f4e
+46 -35
View File
@@ -1,6 +1,7 @@
import { useMemo, useCallback, useState, useRef, useEffect } from 'react';
import { IconRefresh, IconMaximize, IconMinimize } from '@tabler/icons-react';
import { countryToFlag } from './utils/serverUtils.js';
const FLAG_CDN = 'https://flagcdn.com';
const UNIFI_BG = '#0f172a';
const UNIFI_CARD_BG = '#1e293b';
@@ -270,8 +271,11 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
const perpY = (ddx / len) * baseOffset;
const cpx = mx + perpX;
const cpy = my + perpY;
const perpLen = Math.hypot(perpX, perpY) || 1;
const ux = perpX / perpLen;
const uy = perpY / perpLen;
return { sx, sy, ex, ey, cpx, cpy };
return { sx, sy, ex, ey, cpx, cpy, mx, my, perpX: ux, perpY: uy };
}, []);
const pathD = useCallback((e) => {
@@ -279,12 +283,14 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
return `M ${sx} ${sy} Q ${cpx} ${cpy} ${ex} ${ey}`;
}, [computeCurvePoints]);
const labelPoint = useCallback((e) => {
const { sx, sy, ex, ey, cpx, cpy } = computeCurvePoints(e);
const t = 0.5;
const lx = (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cpx + t * t * ex;
const ly = (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cpy + t * t * ey;
return { x: lx, y: ly };
/** Точка для подписи: середина ребра + смещение по перпендикуляру (чтобы подпись была у ребра, а не уезжала) */
const labelPlacement = useCallback((e, level = 0) => {
const { mx, my, perpX, perpY } = computeCurvePoints(e);
const baseOffset = 18;
const stepOffset = 26;
const x = mx + perpX * (baseOffset + level * stepOffset);
const y = my + perpY * (baseOffset + level * stepOffset);
return { x, y };
}, [computeCurvePoints]);
if (servers.length === 0) return null;
@@ -342,24 +348,22 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
opacity={dimFactor}
/>
{(() => {
const base = labelPoint(e);
let x = base.x;
let y = base.y;
const labels = [e.pingLabel, e.speedLabel].filter(Boolean);
if (labels.length === 0) return null;
let level = 0;
const totalH = labels.length * LABEL_H + (labels.length - 1) * LABEL_GAP;
while (
level < 10 &&
used.some(
(p) =>
Math.abs(p.x - x) < LABEL_W &&
Math.abs(p.y - (y + level * (totalH + 8))) < totalH + 16
)
) {
const minDist = 70;
let level = 0;
let x; let y;
while (level < 10) {
const pos = labelPlacement(e, level);
x = pos.x;
y = pos.y;
const overlaps = used.some(
(p) => Math.hypot(p.x - x, p.y - y) < minDist
);
if (!overlaps) break;
level += 1;
}
y = y + level * (totalH + 8);
used.push({ x, y });
const pad = 8;
@@ -418,20 +422,27 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
strokeWidth={1}
/>
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
{/* Флаг страны в левом верхнем углу карточки */}
{s.country && (
<text
x={-NODE_WIDTH / 2 + 14}
y={-NODE_HEIGHT / 2 + 14}
textAnchor="middle"
dominantBaseline="middle"
fontSize={14}
style={{ pointerEvents: 'none' }}
title={s.country}
>
{countryToFlag(s.country)}
</text>
)}
{/* Флаг страны в левом верхнем углу карточки (картинка, т.к. эмодзи в SVG часто отображаются как буквы) */}
{s.country && (() => {
const cc = String(s.country).trim().toLowerCase();
if (cc.length !== 2) return null;
const flagW = 22;
const flagH = 16;
return (
<image
href={`${FLAG_CDN}/w40/${cc}.png`}
x={-NODE_WIDTH / 2 + 6}
y={-NODE_HEIGHT / 2 + 6}
width={flagW}
height={flagH}
preserveAspectRatio="xMidYMid meet"
style={{ pointerEvents: 'none' }}
className="network-map-node-flag"
>
<title>{s.country.toUpperCase()}</title>
</image>
);
})()}
{/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */}
<g transform="translate(-10, -16)">
<rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={UNIFI_GREEN} strokeWidth={1.5} />