diff --git a/Wavelet/frontend/app/(main)/components/dashboard/world-stage-map.tsx b/Wavelet/frontend/app/(main)/components/dashboard/world-stage-map.tsx index 0f456864..c1efaf1f 100644 --- a/Wavelet/frontend/app/(main)/components/dashboard/world-stage-map.tsx +++ b/Wavelet/frontend/app/(main)/components/dashboard/world-stage-map.tsx @@ -6,8 +6,9 @@ import * as echarts from 'echarts/core'; import { MapChart, ScatterChart } from 'echarts/charts'; import { GeoComponent, TooltipComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; -import { useRouter } from 'next/navigation'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import {useTheme} from 'next-themes'; +import {useRouter} from 'next/navigation'; +import {useEffect, useMemo, useRef, useState} from 'react'; import {EmptyState} from '@/components/layout/empty'; import type {DashboardNodeHealth, DistributionItem} from '@/lib/services/openflare'; @@ -192,14 +193,14 @@ function ensureWorldMapRegistered() { } export function WorldStageMap({ - isDark, nodes, sourceCountries, }: { - isDark: boolean; nodes: DashboardNodeHealth[]; sourceCountries: DistributionItem[]; }) { + const {resolvedTheme} = useTheme(); + const isDark = resolvedTheme === 'dark'; const router = useRouter(); const chartContainerRef = useRef(null); const [mapReady, setMapReady] = useState(false); @@ -251,30 +252,26 @@ export function WorldStageMap({ () => isDark ? { - areaColor: '#13233b', - borderColor: 'rgba(125,211,252,0.14)', + areaColor: '#1c1f26', + borderColor: 'rgba(148,163,184,0.16)', labelColor: '#e2e8f0', - sourceAreaColor: 'rgba(56, 189, 248, 0.18)', - sourceAreaBorder: 'rgba(125, 211, 252, 0.45)', healthyColor: '#34d399', warningColor: '#fbbf24', - dangerColor: '#fb7185', - healthyBorder: '#bbf7d0', - warningBorder: '#fde68a', - dangerBorder: '#fecdd3', + dangerColor: '#f87171', + healthyBorder: '#6ee7b7', + warningBorder: '#fcd34d', + dangerBorder: '#fca5a5', } : { - areaColor: '#eaf2ff', - borderColor: 'rgba(71,85,105,0.12)', - labelColor: '#0f172a', - sourceAreaColor: 'rgba(14, 165, 233, 0.22)', - sourceAreaBorder: 'rgba(14, 165, 233, 0.46)', + areaColor: '#f4f4f5', + borderColor: 'rgba(148,163,184,0.28)', + labelColor: '#18181b', healthyColor: '#10b981', warningColor: '#f59e0b', - dangerColor: '#f43f5e', - healthyBorder: '#d1fae5', - warningBorder: '#fde68a', - dangerBorder: '#fecdd3', + dangerColor: '#ef4444', + healthyBorder: '#6ee7b7', + warningBorder: '#fcd34d', + dangerBorder: '#fca5a5', }, [isDark], ); @@ -342,11 +339,11 @@ export function WorldStageMap({ 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})`; + ? `rgba(96, 165, 250, ${areaOpacity})` + : `rgba(59, 130, 246, ${areaOpacity})`; const borderColor = isDark - ? `rgba(125, 211, 252, ${borderOpacity})` - : `rgba(14, 165, 233, ${borderOpacity})`; + ? `rgba(147, 197, 253, ${borderOpacity})` + : `rgba(59, 130, 246, ${borderOpacity})`; return { name: item.key, @@ -397,14 +394,14 @@ export function WorldStageMap({ trigger: 'item', transitionDuration: 0, backgroundColor: isDark - ? 'rgba(15,23,42,0.96)' - : 'rgba(255,255,255,0.96)', + ? 'rgba(24,24,27,0.96)' + : 'rgba(255,255,255,0.98)', borderColor: isDark - ? 'rgba(148,163,184,0.2)' - : 'rgba(148,163,184,0.24)', + ? 'rgba(63,63,70,0.8)' + : 'rgba(228,228,231,0.9)', borderWidth: 1, textStyle: { - color: isDark ? '#e2e8f0' : '#0f172a', + color: isDark ? '#fafafa' : '#18181b', fontSize: 12, }, formatter: (params: unknown) => { @@ -508,11 +505,11 @@ export function WorldStageMap({ distance: 8, formatter: '{b}', backgroundColor: isDark - ? 'rgba(8,15,31,0.7)' - : 'rgba(255,255,255,0.88)', + ? 'rgba(24,24,27,0.88)' + : 'rgba(255,255,255,0.94)', borderColor: isDark - ? 'rgba(148,163,184,0.16)' - : 'rgba(148,163,184,0.2)', + ? 'rgba(63,63,70,0.7)' + : 'rgba(228,228,231,0.9)', borderWidth: 1, borderRadius: 999, padding: [4, 8], diff --git a/Wavelet/frontend/app/(main)/components/dashboard/world-stage.tsx b/Wavelet/frontend/app/(main)/components/dashboard/world-stage.tsx index 519c1e99..19ce2a49 100644 --- a/Wavelet/frontend/app/(main)/components/dashboard/world-stage.tsx +++ b/Wavelet/frontend/app/(main)/components/dashboard/world-stage.tsx @@ -1,11 +1,27 @@ 'use client'; import dynamic from 'next/dynamic'; -import { useEffect, useRef, useState } from 'react'; - -import {useTheme} from 'next-themes'; +import {useEffect, useRef, useState, type ComponentType} from 'react'; +import { + Activity, + Cpu, + Globe2, + HardDrive, + MemoryStick, + Server, + ShieldCheck, +} from 'lucide-react'; import {EmptyState} from '@/components/layout/empty'; +import {Badge} from '@/components/ui/badge'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import {Progress} from '@/components/ui/progress'; import type { DashboardCapacity, DashboardNodeHealth, @@ -15,99 +31,63 @@ import type { } from '@/lib/services/openflare'; import {cn} from '@/lib/utils'; -type Tone = 'healthy' | 'warning' | 'danger' | 'source'; +import {formatCompactNumber, formatPercent} from './dashboard-utils'; const WorldStageMap = dynamic( - () => - import('./world-stage-map').then((module) => module.WorldStageMap), + () => import('./world-stage-map').then((module) => module.WorldStageMap), {ssr: false}, ); -function formatPercent(value: number) { - if (!Number.isFinite(value)) { - return '0%'; - } - return `${value.toFixed(value >= 100 ? 0 : 1)}%`; +type LegendTone = 'source' | 'healthy' | 'warning' | 'danger'; + +const legendToneClass: Record = { + source: 'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400', + healthy: + 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400', + warning: + 'bg-amber-500/10 border-amber-500/20 text-amber-600 dark:text-amber-400', + danger: + 'bg-destructive/10 border-destructive/20 text-destructive', +}; + +function LegendBadge({label, tone}: {label: string; tone: LegendTone}) { + return ( + + {label} + + ); } -function HeroMetric({ +function StageMetricCard({ label, value, hint, - isDark, + icon: Icon, + progress, }: { label: string; value: string; hint: string; - isDark: boolean; + icon: ComponentType<{className?: string}>; + progress?: number; }) { return ( -
-

- {label} -

-

- {value} -

-

- {hint} -

-
- ); -} - -function LegendPill({ - label, - tone, - isDark, -}: { - label: string; - tone: Tone; - isDark: boolean; -}) { - const toneClass = - tone === 'healthy' - ? isDark - ? 'border-emerald-300/20 bg-emerald-400/10 text-emerald-100' - : 'border-emerald-200 bg-emerald-50 text-emerald-700' - : tone === 'warning' - ? isDark - ? 'border-amber-300/20 bg-amber-400/10 text-amber-100' - : 'border-amber-200 bg-amber-50 text-amber-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 ( -
- {label} -
+ + + {label} + + + +
{value}
+ {typeof progress === 'number' ? ( + + ) : null} +

{hint}

+
+
); } @@ -124,8 +104,6 @@ export function WorldStage({ nodes: DashboardNodeHealth[]; sourceCountries: DistributionItem[]; }) { - const {resolvedTheme} = useTheme(); - const isDark = resolvedTheme === 'dark'; const mapViewportRef = useRef(null); const [shouldRenderMap, setShouldRenderMap] = useState(false); @@ -147,7 +125,7 @@ export function WorldStage({ observer.disconnect(); } }, - { rootMargin: '180px 0px' }, + {rootMargin: '180px 0px'}, ); observer.observe(mapViewport); @@ -173,171 +151,108 @@ export function WorldStage({ typeof node.geo_longitude === 'number', ).length; + const mapModeLabel = + sourceCountries.length > 0 + ? '访客来源热度' + : geoConfiguredNodes > 0 + ? '节点地理坐标' + : '节点覆盖信号'; + return ( -
-
-
-
-

- Global Stage -

-

- 全球态势板 -

+
+ + +
+
+ + + 全球态势板 + + + 节点地理分布与访客来源热度一览 + +
+ + {mapModeLabel} +
-
-
- -
-
+
+ + + + +
+ +
-
+ {shouldRenderMap ? ( + + ) : ( + )} - > - {sourceCountries.length > 0 - ? '访客来源热度' - : geoConfiguredNodes > 0 - ? '节点地理坐标' - : '节点覆盖信号'} -
- -
- - - - -
- -
-
- {shouldRenderMap ? ( - - ) : ( -
- -
- )} -
{shouldRenderMap && nodes.length === 0 ? ( -
-
+
+
当前暂无节点接入。地图已完成初始化,后续会在这里展示节点位置与健康状态。
) : null}
-
+ + -
-
- - - - - - -
-
+
+ + + + + +
-
+ ); -} +} \ No newline at end of file diff --git a/docs/changelog/index.md b/docs/changelog/index.md index dc66ea4d..94da27e4 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -20,6 +20,7 @@ sidebar: false - Wavelet 总览仪表盘图表对齐旧前端 ECharts 样式:24 小时请求/容量趋势补充请求量与错误量展示,新增网络与磁盘趋势、Top 节点榜单、来源分布、状态码分布与 Top Domain 板块。 - Wavelet 总览仪表盘迁移旧前端「全球态势板」:世界地图展示节点健康与来源国家热度,并保留在线覆盖、流量与容量摘要指标。 +- Wavelet 全球态势板视觉对齐系统风格:改用 `Card` / `Badge` / `Progress` 与虚线边框指标卡,地图配色与 tooltip 跟随主题 token。 - 将 `openflare-server` 后端业务域迁移至 `Wavelet/internal/apps/openflare/`,通过 `/api/*` legacy 兼容层保持旧前端 API 路径不变。 - 新增 OpenFlare 业务表 goose 迁移(`of_options`、`of_origins`、`of_proxy_routes`、`of_nodes`、`of_waf_*`、`of_tls_*`、`of_config_versions`、`of_pages_*`、`of_apply_logs` 等)。