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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user