重构仪表板组件,移除冗余的 OverviewMetric 组件,优化布局和样式,增强可读性

This commit is contained in:
ryan
2026-03-14 16:28:56 +08:00
parent 590e1d7a8a
commit 271ac772c4
3 changed files with 180 additions and 110 deletions
@@ -84,49 +84,6 @@ function buildNodeRankItems(
}));
}
function OverviewMetric({
label,
value,
hint,
accent = 'sky',
}: {
label: string;
value: string | number;
hint?: string;
accent?: 'sky' | 'amber' | 'emerald' | 'rose';
}) {
const accentClass =
accent === 'amber'
? 'from-amber-400/14 to-transparent'
: accent === 'emerald'
? 'from-emerald-400/14 to-transparent'
: accent === 'rose'
? 'from-rose-400/14 to-transparent'
: 'from-sky-400/14 to-transparent';
return (
<div
className={`rounded-3xl border border-[var(--border-default)] bg-[linear-gradient(180deg,var(--surface-elevated),var(--surface-card))] px-5 py-5 shadow-[var(--shadow-soft)]`}
>
<div className={`rounded-2xl bg-gradient-to-r ${accentClass} p-0.5`}>
<div className="rounded-[15px] bg-transparent">
<p className="text-xs tracking-[0.24em] text-[var(--foreground-muted)] uppercase">
{label}
</p>
<p className="mt-3 text-3xl font-semibold text-[var(--foreground-primary)]">
{value}
</p>
{hint ? (
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
{hint}
</p>
) : null}
</div>
</div>
</div>
);
}
function NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
return (
<Link
@@ -242,50 +199,6 @@ export function DashboardOverview() {
sourceCountries={overview.distributions.source_countries}
/>
<AppCard
title="系统健康摘要"
description="聚焦在线率、运行质量、流量体量与资源负载,保留首屏最核心的运营信号。"
>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<OverviewMetric
label="在线节点"
value={`${overview.summary.online_nodes}/${overview.summary.total_nodes}`}
hint={`${overview.summary.offline_nodes} 离线 · ${overview.summary.pending_nodes} 待接入`}
accent="sky"
/>
<OverviewMetric
label="OpenResty 异常"
value={overview.summary.unhealthy_nodes}
hint="当前运行态已出现不健康节点"
accent="rose"
/>
<OverviewMetric
label="最近窗口请求"
value={overview.traffic.request_count.toLocaleString('zh-CN')}
hint={`QPS ${overview.traffic.estimated_qps.toFixed(1)} · UV ${overview.traffic.unique_visitors.toLocaleString('zh-CN')}`}
accent="emerald"
/>
<OverviewMetric
label="平均 CPU"
value={formatPercent(overview.capacity.average_cpu_usage_percent)}
hint={`${overview.capacity.high_cpu_nodes} 个高 CPU 节点`}
accent="amber"
/>
<OverviewMetric
label="平均内存"
value={formatPercent(overview.capacity.average_memory_usage_percent)}
hint={`${overview.capacity.high_memory_nodes} 个高内存节点`}
accent="sky"
/>
<OverviewMetric
label="高存储节点"
value={overview.capacity.high_storage_nodes}
hint={`${overview.traffic.reported_nodes} 个节点参与当前窗口上报`}
accent="amber"
/>
</div>
</AppCard>
<div className="grid gap-6 xl:grid-cols-2">
<AppCard
title="24 小时请求趋势"
@@ -36,7 +36,17 @@ type Tone = 'healthy' | 'warning' | 'danger';
type WorldGeoJson = {
type: string;
features?: unknown[];
features?: WorldFeature[];
};
type WorldFeature = {
properties?: {
name?: string;
};
geometry?: {
type?: 'Polygon' | 'MultiPolygon';
coordinates?: number[][][] | number[][][][];
};
};
type MapNodeDatum = {
@@ -64,6 +74,20 @@ type MapNodeDatum = {
};
};
type CountryRegionDatum = {
name: string;
itemStyle: {
areaColor: string;
borderColor: string;
};
emphasis: {
itemStyle: {
areaColor: string;
borderColor: string;
};
};
};
let worldMapRegistrationAttempted = false;
let worldMapRegistrationSucceeded = false;
@@ -152,6 +176,70 @@ function ensureWorldMapRegistered() {
}
}
function isPointInRing(point: [number, number], ring: number[][]) {
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
const [xi, yi] = ring[i] ?? [];
const [xj, yj] = ring[j] ?? [];
if (
yi > point[1] !== yj > point[1] &&
point[0] < ((xj - xi) * (point[1] - yi)) / (yj - yi || Number.EPSILON) + xi
) {
inside = !inside;
}
}
return inside;
}
function isPointInPolygon(point: [number, number], rings: number[][][]) {
if (rings.length === 0 || !isPointInRing(point, rings[0] ?? [])) {
return false;
}
for (let index = 1; index < rings.length; index += 1) {
if (isPointInRing(point, rings[index] ?? [])) {
return false;
}
}
return true;
}
function getCountryNameByCoordinates(
coordinates: [number, number],
geoJson: WorldGeoJson,
) {
for (const feature of geoJson.features ?? []) {
const name = feature.properties?.name;
const geometryType = feature.geometry?.type;
const geometryCoordinates = feature.geometry?.coordinates;
if (!name || !geometryType || !geometryCoordinates) {
continue;
}
if (
geometryType === 'Polygon' &&
isPointInPolygon(coordinates, geometryCoordinates as number[][][])
) {
return name;
}
if (geometryType === 'MultiPolygon') {
for (const polygon of geometryCoordinates as number[][][][]) {
if (isPointInPolygon(coordinates, polygon)) {
return name;
}
}
}
}
return null;
}
export function WorldStageMap({
isDark,
nodes,
@@ -176,6 +264,10 @@ export function WorldStageMap({
areaColor: '#13233b',
borderColor: 'rgba(125,211,252,0.14)',
labelColor: '#e2e8f0',
healthyAreaColor: 'rgba(74, 222, 128, 0.18)',
dangerAreaColor: 'rgba(251, 113, 133, 0.18)',
healthyAreaBorder: 'rgba(134, 239, 172, 0.4)',
dangerAreaBorder: 'rgba(253, 164, 175, 0.42)',
healthyColor: '#34d399',
warningColor: '#fbbf24',
dangerColor: '#fb7185',
@@ -187,6 +279,10 @@ export function WorldStageMap({
areaColor: '#eaf2ff',
borderColor: 'rgba(71,85,105,0.12)',
labelColor: '#0f172a',
healthyAreaColor: '#dcfce7',
dangerAreaColor: '#ffe4e6',
healthyAreaBorder: '#86efac',
dangerAreaBorder: '#fda4af',
healthyColor: '#10b981',
warningColor: '#f59e0b',
dangerColor: '#f43f5e',
@@ -243,6 +339,56 @@ export function WorldStageMap({
[mapPalette, nodes],
);
const countryRegions = useMemo<CountryRegionDatum[]>(() => {
const geoJson = worldGeoJson as WorldGeoJson;
const countryToneMap = new Map<string, Tone>();
nodes.forEach((node, index) => {
const { coordinates, derivedFromGeo } = getNodeCoordinates(node, index);
if (!derivedFromGeo) {
return;
}
const countryName = getCountryNameByCoordinates(coordinates, geoJson);
if (!countryName) {
return;
}
const nextTone = getNodeTone(node);
const normalizedTone = nextTone === 'healthy' ? 'healthy' : 'danger';
const currentTone = countryToneMap.get(countryName);
if (!currentTone || currentTone === 'healthy') {
countryToneMap.set(countryName, normalizedTone);
}
});
return Array.from(countryToneMap.entries()).map(([name, tone]) => {
const areaColor =
tone === 'healthy'
? mapPalette.healthyAreaColor
: mapPalette.dangerAreaColor;
const borderColor =
tone === 'healthy'
? mapPalette.healthyAreaBorder
: mapPalette.dangerAreaBorder;
return {
name,
itemStyle: {
areaColor,
borderColor,
},
emphasis: {
itemStyle: {
areaColor,
borderColor,
},
},
};
});
}, [mapPalette, nodes]);
const mapOption = useMemo<EChartsCoreOption>(
() => ({
animation: false,
@@ -287,6 +433,7 @@ export function WorldStageMap({
layoutCenter: ['50%', '50%'],
layoutSize: '180%',
zoom: 1,
regions: countryRegions,
itemStyle: {
areaColor: mapPalette.areaColor,
borderColor: mapPalette.borderColor,
@@ -334,7 +481,7 @@ export function WorldStageMap({
},
],
}),
[isDark, mapNodes, mapPalette],
[countryRegions, isDark, mapNodes, mapPalette],
);
if (!mapReady) {
@@ -233,18 +233,28 @@ export function WorldStage({
)}
>
<div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<p
className={cn(
'text-[11px] tracking-[0.34em] uppercase',
isDark ? 'text-sky-200/80' : 'text-sky-700/80',
)}
>
Global Stage
</p>
<div className="space-y-2">
<p
className={cn(
'text-[11px] tracking-[0.34em] uppercase',
isDark ? 'text-sky-200/80' : 'text-sky-700/80',
)}
>
Global Stage
</p>
<h2
className={cn(
'text-2xl font-semibold',
isDark ? 'text-white' : 'text-slate-950',
)}
>
全球态势板
</h2>
</div>
</div>
</div>
<div className="grid gap-6 px-6 py-7 md:px-7 md:py-8 xl:grid-cols-[1.4fr_0.8fr]">
<div className="grid gap-6 px-6 py-7 md:px-7 md:py-8 xl:grid-cols-[1.32fr_0.88fr]">
<div className="space-y-5">
<div
className={cn(
@@ -328,18 +338,6 @@ export function WorldStage({
)}
</div>
</div>
<div
className={cn(
'rounded-[24px] border px-5 py-4 text-xs leading-6 backdrop-blur',
isDark
? 'border-white/10 bg-white/5 text-slate-300'
: 'border-slate-200/80 bg-white/80 text-slate-600',
)}
>
当前已有 {geoConfiguredNodes}/{summary.total_nodes} 个节点配置了地理坐标。
未配置坐标的节点会映射到预设城市点位,用于维持全球覆盖视图的连续性。
</div>
</div>
<div className="space-y-5">
@@ -368,6 +366,18 @@ export function WorldStage({
hint={`${capacity.high_cpu_nodes} 个节点 CPU 偏高`}
isDark={isDark}
/>
<HeroMetric
label="平均内存"
value={formatPercent(capacity.average_memory_usage_percent)}
hint={`${capacity.high_memory_nodes} 个高内存节点`}
isDark={isDark}
/>
<HeroMetric
label="高存储节点"
value={capacity.high_storage_nodes.toLocaleString('zh-CN')}
hint={`${summary.offline_nodes} 离线 · ${summary.pending_nodes} 待接入`}
isDark={isDark}
/>
</div>
</div>
</div>