From d68773c5540dd157c3cfca28b8877fcc02049f09 Mon Sep 17 00:00:00 2001
From: ryan
Date: Sun, 15 Mar 2026 16:00:44 +0800
Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E6=9D=A5=E6=BA=90=E7=83=AD=E5=BA=A6=E6=98=BE=E7=A4=BA=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E6=95=B0?=
=?UTF-8?q?=E6=8D=AE=E7=BB=93=E6=9E=84=E5=92=8C=E5=89=8D=E7=AB=AF=E7=BB=84?=
=?UTF-8?q?=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../dashboard/components/world-stage-map.tsx | 229 ++++++++----------
.../dashboard/components/world-stage.tsx | 158 +++++-------
2 files changed, 160 insertions(+), 227 deletions(-)
diff --git a/atsf_server/web/features/dashboard/components/world-stage-map.tsx b/atsf_server/web/features/dashboard/components/world-stage-map.tsx
index f0262437..ff138031 100644
--- a/atsf_server/web/features/dashboard/components/world-stage-map.tsx
+++ b/atsf_server/web/features/dashboard/components/world-stage-map.tsx
@@ -11,7 +11,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { EmptyState } from '@/components/feedback/empty-state';
import worldGeoJson from '@/features/dashboard/data/world-geo.json';
-import type { DashboardNodeHealth } from '@/features/dashboard/types';
+import type {
+ DashboardNodeHealth,
+ DistributionItem,
+} from '@/features/dashboard/types';
import {
getNodeStatusLabel,
getOpenrestyStatusLabel,
@@ -76,6 +79,7 @@ type MapNodeDatum = {
type CountryRegionDatum = {
name: string;
+ value: number;
itemStyle: {
areaColor: string;
borderColor: string;
@@ -133,10 +137,9 @@ function getNodeCoordinates(node: DashboardNodeHealth, index: number) {
}
return {
- coordinates: [...fallbackCoordinates[index % fallbackCoordinates.length]] as [
- number,
- number,
- ],
+ coordinates: [
+ ...fallbackCoordinates[index % fallbackCoordinates.length],
+ ] as [number, number],
derivedFromGeo: false,
};
}
@@ -156,7 +159,10 @@ function ensureWorldMapRegistered() {
try {
const geoJson = worldGeoJson as WorldGeoJson;
- if (geoJson.type !== 'FeatureCollection' || !Array.isArray(geoJson.features)) {
+ if (
+ geoJson.type !== 'FeatureCollection' ||
+ !Array.isArray(geoJson.features)
+ ) {
throw new Error('invalid world geojson payload');
}
@@ -173,82 +179,22 @@ function ensureWorldMapRegistered() {
return true;
} catch (error) {
const registrationError =
- error instanceof Error ? error : new Error('unknown world map registration error');
+ error instanceof Error
+ ? error
+ : new Error('unknown world map registration error');
console.error('Failed to register ECharts world map', registrationError);
return false;
}
}
-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,
+ sourceCountries,
}: {
isDark: boolean;
nodes: DashboardNodeHealth[];
+ sourceCountries: DistributionItem[];
}) {
const router = useRouter();
const chartContainerRef = useRef(null);
@@ -304,10 +250,8 @@ 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)',
+ sourceAreaColor: 'rgba(56, 189, 248, 0.18)',
+ sourceAreaBorder: 'rgba(125, 211, 252, 0.45)',
healthyColor: '#34d399',
warningColor: '#fbbf24',
dangerColor: '#fb7185',
@@ -319,10 +263,8 @@ export function WorldStageMap({
areaColor: '#eaf2ff',
borderColor: 'rgba(71,85,105,0.12)',
labelColor: '#0f172a',
- healthyAreaColor: '#dcfce7',
- dangerAreaColor: '#ffe4e6',
- healthyAreaBorder: '#86efac',
- dangerAreaBorder: '#fda4af',
+ sourceAreaColor: 'rgba(14, 165, 233, 0.22)',
+ sourceAreaBorder: 'rgba(14, 165, 233, 0.46)',
healthyColor: '#10b981',
warningColor: '#f59e0b',
dangerColor: '#f43f5e',
@@ -362,7 +304,11 @@ export function WorldStageMap({
activeEventCount: node.active_event_count,
status: node.status,
openrestyStatus: node.openresty_status,
- value: [coordinates[0], coordinates[1], Math.max(node.request_count, 1)],
+ value: [
+ coordinates[0],
+ coordinates[1],
+ Math.max(node.request_count, 1),
+ ],
itemStyle: {
color: toneColor,
borderColor: toneBorder,
@@ -380,54 +326,40 @@ export function WorldStageMap({
);
const countryRegions = useMemo(() => {
- const geoJson = worldGeoJson as WorldGeoJson;
- const countryToneMap = new Map();
+ const maxRequests = Math.max(
+ 1,
+ ...sourceCountries.map((item) => item.value || 0),
+ );
- nodes.forEach((node, index) => {
- const { coordinates, derivedFromGeo } = getNodeCoordinates(node, index);
- if (!derivedFromGeo) {
- return;
- }
+ return sourceCountries
+ .filter((item) => item.key && item.value > 0)
+ .map((item) => {
+ const intensity = Math.max(0.18, item.value / maxRequests);
+ const areaOpacity = Number((0.14 + intensity * 0.58).toFixed(3));
+ const borderOpacity = Number((0.22 + intensity * 0.48).toFixed(3));
+ const areaColor = isDark
+ ? `rgba(56, 189, 248, ${areaOpacity})`
+ : `rgba(14, 165, 233, ${areaOpacity})`;
+ const borderColor = isDark
+ ? `rgba(125, 211, 252, ${borderOpacity})`
+ : `rgba(14, 165, 233, ${borderOpacity})`;
- 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: {
+ return {
+ name: item.key,
+ value: item.value,
itemStyle: {
areaColor,
borderColor,
},
- },
- };
- });
- }, [mapPalette, nodes]);
+ emphasis: {
+ itemStyle: {
+ areaColor,
+ borderColor,
+ },
+ },
+ };
+ });
+ }, [isDark, sourceCountries]);
const responsiveMapScale = useMemo(() => {
const { width, height } = containerSize;
@@ -472,8 +404,25 @@ export function WorldStageMap({
fontSize: 12,
},
formatter: (params: unknown) => {
- const data = (params as { data?: MapNodeDatum }).data;
- if (!data) {
+ const payload = params as {
+ data?: MapNodeDatum | CountryRegionDatum;
+ seriesType?: string;
+ name?: string;
+ };
+ const data = payload.data;
+ if (
+ payload.seriesType !== 'scatter' &&
+ data &&
+ 'value' in data &&
+ typeof data.value === 'number'
+ ) {
+ return [
+ `${payload.name ?? data.name}
`,
+ `最近 24 小时来源请求 ${data.value.toLocaleString('zh-CN')}
`,
+ ].join('');
+ }
+
+ if (!data || !('requestCount' in data)) {
return '';
}
@@ -512,17 +461,35 @@ export function WorldStageMap({
},
},
series: [
+ {
+ type: 'map',
+ map: 'world',
+ geoIndex: 0,
+ data: countryRegions,
+ silent: true,
+ z: 1,
+ emphasis: {
+ disabled: true,
+ },
+ },
{
type: 'scatter',
coordinateSystem: 'geo',
data: mapNodes,
+ z: 3,
progressive: 64,
large: true,
largeThreshold: 24,
symbolSize: (value: unknown) => {
- const size = Array.isArray(value) && typeof value[2] === 'number' ? value[2] : 1;
+ const size =
+ Array.isArray(value) && typeof value[2] === 'number'
+ ? value[2]
+ : 1;
const responsiveBase = 8 + Math.log10(size + 1) * 3.6;
- return Math.max(7, Math.min(18, responsiveBase * Math.max(responsiveMapScale, 0.88)));
+ return Math.max(
+ 7,
+ Math.min(18, responsiveBase * Math.max(responsiveMapScale, 0.88)),
+ );
},
label: {
show: false,
@@ -550,7 +517,15 @@ export function WorldStageMap({
},
],
}),
- [computedLayoutSize, computedZoom, countryRegions, isDark, mapNodes, mapPalette, responsiveMapScale],
+ [
+ computedLayoutSize,
+ computedZoom,
+ countryRegions,
+ isDark,
+ mapNodes,
+ mapPalette,
+ responsiveMapScale,
+ ],
);
if (!mapReady) {
diff --git a/atsf_server/web/features/dashboard/components/world-stage.tsx b/atsf_server/web/features/dashboard/components/world-stage.tsx
index 1afb6201..5eb70a86 100644
--- a/atsf_server/web/features/dashboard/components/world-stage.tsx
+++ b/atsf_server/web/features/dashboard/components/world-stage.tsx
@@ -14,7 +14,7 @@ import type {
} from '@/features/dashboard/types';
import { cn } from '@/lib/utils/cn';
-type Tone = 'healthy' | 'warning' | 'danger';
+type Tone = 'healthy' | 'warning' | 'danger' | 'source';
const WorldStageMap = dynamic(
() =>
@@ -61,13 +61,18 @@ function HeroMetric({
{value}
-
+
{hint}
@@ -92,9 +97,13 @@ function LegendPill({
? isDark
? 'border-amber-300/20 bg-amber-400/10 text-amber-100'
: 'border-amber-200 bg-amber-50 text-amber-700'
- : isDark
- ? 'border-rose-300/20 bg-rose-400/10 text-rose-100'
- : 'border-rose-200 bg-rose-50 text-rose-700';
+ : tone === 'source'
+ ? isDark
+ ? 'border-sky-300/20 bg-sky-400/10 text-sky-100'
+ : 'border-sky-200 bg-sky-50 text-sky-700'
+ : isDark
+ ? 'border-rose-300/20 bg-rose-400/10 text-rose-100'
+ : 'border-rose-200 bg-rose-50 text-rose-700';
return (
@@ -103,58 +112,6 @@ function LegendPill({
);
}
-function CountrySignal({
- item,
- index,
- isDark,
-}: {
- item: DistributionItem;
- index: number;
- isDark: boolean;
-}) {
- const darkAccents = [
- 'from-sky-400/35 to-cyan-400/10',
- 'from-violet-400/35 to-fuchsia-400/10',
- 'from-emerald-400/35 to-teal-400/10',
- ];
- const lightAccents = [
- 'from-sky-100 via-white to-cyan-50',
- 'from-indigo-100 via-white to-fuchsia-50',
- 'from-emerald-100 via-white to-teal-50',
- ];
-
- return (
-
-
- {item.key}
-
-
- {item.value.toLocaleString('zh-CN')}
-
-
- 最近 24 小时来源信号
-
-
- );
-}
-
export function WorldStage({
summary,
traffic,
@@ -235,18 +192,18 @@ export function WorldStage({
Global Stage
全球态势板
@@ -266,18 +223,22 @@ export function WorldStage({
>
- {geoConfiguredNodes > 0 ? '节点地理坐标' : '节点覆盖信号'}
+ {sourceCountries.length > 0
+ ? '访客来源热度'
+ : geoConfiguredNodes > 0
+ ? '节点地理坐标'
+ : '节点覆盖信号'}
-
-
-
-
+
+
+
+
+
{shouldRenderMap ? (
-
+
) : (
{shouldRenderMap && nodes.length === 0 ? (
-
+
) : null}
-
-
- {sourceCountries.length > 0 ? (
- sourceCountries.slice(0, 3).map((item, index) => (
-
- ))
- ) : (
-
- 暂无可用于全球来源展示的国家分布数据。
-
- )}
-