mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
重构仪表板组件,移除冗余的 OverviewMetric 组件,优化布局和样式,增强可读性
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user