mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 09:46:37 +08:00
merge: 全球态势板布局紧凑化
This commit is contained in:
@@ -368,9 +368,9 @@ export function WorldStageMap({
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const widthScale = Math.min(Math.max(width / 960, 0.52), 1.08);
|
const widthScale = Math.min(Math.max(width / 720, 0.48), 1.02);
|
||||||
const heightScale = Math.min(Math.max(height / 520, 0.72), 1.1);
|
const heightScale = Math.min(Math.max(height / 260, 0.65), 1.05);
|
||||||
const compactViewportScale = width < 640 ? 0.9 : 1;
|
const compactViewportScale = width < 640 ? 0.88 : 1;
|
||||||
|
|
||||||
return Number(
|
return Number(
|
||||||
(Math.min(widthScale, heightScale) * compactViewportScale).toFixed(3),
|
(Math.min(widthScale, heightScale) * compactViewportScale).toFixed(3),
|
||||||
|
|||||||
@@ -38,56 +38,47 @@ const WorldStageMap = dynamic(
|
|||||||
{ssr: false},
|
{ssr: false},
|
||||||
);
|
);
|
||||||
|
|
||||||
type LegendTone = 'source' | 'healthy' | 'warning' | 'danger';
|
const LEGEND_ITEMS = [
|
||||||
|
{dot: 'bg-blue-500', label: '来源'},
|
||||||
|
{dot: 'bg-emerald-500', label: '正常'},
|
||||||
|
{dot: 'bg-amber-500', label: '承压'},
|
||||||
|
{dot: 'bg-destructive', label: '异常'},
|
||||||
|
] as const;
|
||||||
|
|
||||||
const legendToneClass: Record<LegendTone, string> = {
|
function CompactMetric({
|
||||||
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 (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={cn('text-[10px] font-medium', legendToneClass[tone])}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StageMetricCard({
|
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
hint,
|
hint,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
progress,
|
progress,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
hint: string;
|
hint: string;
|
||||||
icon: ComponentType<{className?: string}>;
|
icon: ComponentType<{className?: string}>;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="border-dashed shadow-none">
|
<div
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
className={cn(
|
||||||
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
'flex min-w-0 flex-col gap-1 rounded-lg border border-dashed bg-muted/15 px-2.5 py-2 lg:rounded-none lg:border-0 lg:bg-transparent lg:px-3 lg:py-2.5',
|
||||||
<Icon className="size-4 text-primary" />
|
className,
|
||||||
</CardHeader>
|
)}
|
||||||
<CardContent className="space-y-2">
|
>
|
||||||
<div className="text-2xl font-semibold tracking-tight">{value}</div>
|
<div className="flex items-center justify-between gap-1.5">
|
||||||
{typeof progress === 'number' ? (
|
<span className="truncate text-[10px] text-muted-foreground">{label}</span>
|
||||||
<Progress value={progress} className="h-1.5" />
|
<Icon className="size-3 shrink-0 text-primary/60" />
|
||||||
) : null}
|
</div>
|
||||||
<p className="text-[10px] text-muted-foreground">{hint}</p>
|
<span className="text-base font-semibold tabular-nums leading-none tracking-tight">
|
||||||
</CardContent>
|
{value}
|
||||||
</Card>
|
</span>
|
||||||
|
{typeof progress === 'number' ? (
|
||||||
|
<Progress value={progress} className="h-0.5" />
|
||||||
|
) : null}
|
||||||
|
<span className="truncate text-[10px] text-muted-foreground">{hint}</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +116,7 @@ export function WorldStage({
|
|||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{rootMargin: '180px 0px'},
|
{rootMargin: '120px 0px'},
|
||||||
);
|
);
|
||||||
|
|
||||||
observer.observe(mapViewport);
|
observer.observe(mapViewport);
|
||||||
@@ -153,106 +144,123 @@ export function WorldStage({
|
|||||||
|
|
||||||
const mapModeLabel =
|
const mapModeLabel =
|
||||||
sourceCountries.length > 0
|
sourceCountries.length > 0
|
||||||
? '访客来源热度'
|
? '访客来源'
|
||||||
: geoConfiguredNodes > 0
|
: geoConfiguredNodes > 0
|
||||||
? '节点地理坐标'
|
? '节点坐标'
|
||||||
: '节点覆盖信号';
|
: '覆盖信号';
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
label: '在线覆盖',
|
||||||
|
value: formatPercent(onlineRate),
|
||||||
|
hint: `${summary.online_nodes}/${summary.total_nodes} 在线`,
|
||||||
|
icon: Server,
|
||||||
|
progress: onlineRate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '运行健康',
|
||||||
|
value: formatPercent(healthyRate),
|
||||||
|
hint: `${summary.unhealthy_nodes} 个异常`,
|
||||||
|
icon: ShieldCheck,
|
||||||
|
progress: healthyRate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '窗口请求',
|
||||||
|
value: formatCompactNumber(traffic.request_count),
|
||||||
|
hint: `QPS ${traffic.estimated_qps.toFixed(1)}`,
|
||||||
|
icon: Activity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '平均 CPU',
|
||||||
|
value: formatPercent(capacity.average_cpu_usage_percent),
|
||||||
|
hint: `${capacity.high_cpu_nodes} 个偏高`,
|
||||||
|
icon: Cpu,
|
||||||
|
progress: capacity.average_cpu_usage_percent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '平均内存',
|
||||||
|
value: formatPercent(capacity.average_memory_usage_percent),
|
||||||
|
hint: `${capacity.high_memory_nodes} 个偏高`,
|
||||||
|
icon: MemoryStick,
|
||||||
|
progress: capacity.average_memory_usage_percent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '高存储',
|
||||||
|
value: formatCompactNumber(capacity.high_storage_nodes),
|
||||||
|
hint: `${summary.offline_nodes} 离线 · ${summary.pending_nodes} 待接入`,
|
||||||
|
icon: HardDrive,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 xl:grid-cols-[1.32fr_0.88fr]">
|
<Card className="border-dashed shadow-none">
|
||||||
<Card className="border-dashed shadow-none">
|
<CardHeader className="gap-2 space-y-0 pb-3">
|
||||||
<CardHeader className="space-y-3">
|
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="min-w-0">
|
||||||
<div>
|
<CardTitle className="flex items-center gap-1.5 text-sm font-semibold">
|
||||||
<CardTitle className="text-sm font-semibold flex items-center gap-1.5">
|
<Globe2 className="size-4 shrink-0 text-primary" />
|
||||||
<Globe2 className="size-4 text-primary" />
|
全球态势板
|
||||||
全球态势板
|
<Badge
|
||||||
</CardTitle>
|
variant="outline"
|
||||||
<CardDescription className="text-xs">
|
className="ml-1 text-[10px] font-normal text-muted-foreground"
|
||||||
节点地理分布与访客来源热度一览
|
>
|
||||||
</CardDescription>
|
{mapModeLabel}
|
||||||
</div>
|
</Badge>
|
||||||
<Badge variant="outline" className="text-[10px] text-muted-foreground">
|
</CardTitle>
|
||||||
{mapModeLabel}
|
<CardDescription className="text-xs">
|
||||||
</Badge>
|
节点分布与来源热度
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
<LegendBadge label="国家底色: 来源热度" tone="source" />
|
{LEGEND_ITEMS.map((item) => (
|
||||||
<LegendBadge label="绿色: 运行正常" tone="healthy" />
|
<span
|
||||||
<LegendBadge label="黄色: 资源承压" tone="warning" />
|
key={item.label}
|
||||||
<LegendBadge label="红色: 异常待处理" tone="danger" />
|
className="inline-flex items-center gap-1 text-[10px] text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span className={cn('size-1.5 rounded-full', item.dot)} />
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_12.5rem] lg:gap-4">
|
||||||
<div
|
<div
|
||||||
ref={mapViewportRef}
|
ref={mapViewportRef}
|
||||||
className="relative min-h-[480px] overflow-hidden rounded-lg border border-dashed bg-muted/20 lg:min-h-[540px]"
|
className="relative h-[200px] overflow-hidden rounded-lg border border-dashed bg-muted/20 sm:h-[240px] lg:h-[260px]"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 flex items-center justify-center p-4">
|
<div className="absolute inset-0">
|
||||||
{shouldRenderMap ? (
|
{shouldRenderMap ? (
|
||||||
<WorldStageMap nodes={nodes} sourceCountries={sourceCountries} />
|
<WorldStageMap nodes={nodes} sourceCountries={sourceCountries} />
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<div className="flex h-full items-center justify-center px-4">
|
||||||
title="全球地图准备中"
|
<EmptyState
|
||||||
description="地图会在进入可视区域后再加载,避免首页滚动和交互被首屏图表拖慢。"
|
title="地图准备中"
|
||||||
iconSize="sm"
|
description="进入可视区域后加载"
|
||||||
/>
|
iconSize="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shouldRenderMap && nodes.length === 0 ? (
|
{shouldRenderMap && nodes.length === 0 ? (
|
||||||
<div className="pointer-events-none absolute inset-x-4 bottom-4 z-10">
|
<div className="pointer-events-none absolute inset-x-3 bottom-2 z-10">
|
||||||
<div className="rounded-lg border border-dashed bg-background/90 px-4 py-3 text-xs text-muted-foreground backdrop-blur-sm">
|
<p className="rounded-md border border-dashed bg-background/90 px-2.5 py-1.5 text-[10px] text-muted-foreground backdrop-blur-sm">
|
||||||
当前暂无节点接入。地图已完成初始化,后续会在这里展示节点位置与健康状态。
|
暂无节点,接入后将展示地理分布
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-1 lg:gap-0 lg:divide-y lg:divide-dashed lg:overflow-hidden lg:rounded-lg lg:border lg:border-dashed">
|
||||||
<StageMetricCard
|
{metrics.map((metric) => (
|
||||||
label="在线覆盖"
|
<CompactMetric key={metric.label} {...metric} />
|
||||||
value={formatPercent(onlineRate)}
|
))}
|
||||||
hint={`${summary.online_nodes}/${summary.total_nodes} 个节点在线`}
|
</div>
|
||||||
icon={Server}
|
</div>
|
||||||
progress={onlineRate}
|
</CardContent>
|
||||||
/>
|
</Card>
|
||||||
<StageMetricCard
|
|
||||||
label="运行健康"
|
|
||||||
value={formatPercent(healthyRate)}
|
|
||||||
hint={`${summary.unhealthy_nodes} 个节点存在 OpenResty 异常`}
|
|
||||||
icon={ShieldCheck}
|
|
||||||
progress={healthyRate}
|
|
||||||
/>
|
|
||||||
<StageMetricCard
|
|
||||||
label="最近窗口请求"
|
|
||||||
value={formatCompactNumber(traffic.request_count)}
|
|
||||||
hint={`QPS ${traffic.estimated_qps.toFixed(1)} · ${traffic.reported_nodes} 个节点已上报`}
|
|
||||||
icon={Activity}
|
|
||||||
/>
|
|
||||||
<StageMetricCard
|
|
||||||
label="平均 CPU"
|
|
||||||
value={formatPercent(capacity.average_cpu_usage_percent)}
|
|
||||||
hint={`${capacity.high_cpu_nodes} 个节点 CPU 偏高`}
|
|
||||||
icon={Cpu}
|
|
||||||
progress={capacity.average_cpu_usage_percent}
|
|
||||||
/>
|
|
||||||
<StageMetricCard
|
|
||||||
label="平均内存"
|
|
||||||
value={formatPercent(capacity.average_memory_usage_percent)}
|
|
||||||
hint={`${capacity.high_memory_nodes} 个高内存节点`}
|
|
||||||
icon={MemoryStick}
|
|
||||||
progress={capacity.average_memory_usage_percent}
|
|
||||||
/>
|
|
||||||
<StageMetricCard
|
|
||||||
label="高存储节点"
|
|
||||||
value={formatCompactNumber(capacity.high_storage_nodes)}
|
|
||||||
hint={`${summary.offline_nodes} 离线 · ${summary.pending_nodes} 待接入`}
|
|
||||||
icon={HardDrive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ sidebar: false
|
|||||||
- Wavelet 总览仪表盘图表对齐旧前端 ECharts 样式:24 小时请求/容量趋势补充请求量与错误量展示,新增网络与磁盘趋势、Top 节点榜单、来源分布、状态码分布与 Top Domain 板块。
|
- Wavelet 总览仪表盘图表对齐旧前端 ECharts 样式:24 小时请求/容量趋势补充请求量与错误量展示,新增网络与磁盘趋势、Top 节点榜单、来源分布、状态码分布与 Top Domain 板块。
|
||||||
- Wavelet 总览仪表盘迁移旧前端「全球态势板」:世界地图展示节点健康与来源国家热度,并保留在线覆盖、流量与容量摘要指标。
|
- Wavelet 总览仪表盘迁移旧前端「全球态势板」:世界地图展示节点健康与来源国家热度,并保留在线覆盖、流量与容量摘要指标。
|
||||||
- Wavelet 全球态势板视觉对齐系统风格:改用 `Card` / `Badge` / `Progress` 与虚线边框指标卡,地图配色与 tooltip 跟随主题 token。
|
- Wavelet 全球态势板视觉对齐系统风格:改用 `Card` / `Badge` / `Progress` 与虚线边框指标卡,地图配色与 tooltip 跟随主题 token。
|
||||||
|
- Wavelet 全球态势板布局紧凑化:地图高度降至约 260px,指标改为侧栏密集单元格,图例改为行内色点标注。
|
||||||
|
|
||||||
- 将 `openflare-server` 后端业务域迁移至 `Wavelet/internal/apps/openflare/`,通过 `/api/*` legacy 兼容层保持旧前端 API 路径不变。
|
- 将 `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` 等)。
|
- 新增 OpenFlare 业务表 goose 迁移(`of_options`、`of_origins`、`of_proxy_routes`、`of_nodes`、`of_waf_*`、`of_tls_*`、`of_config_versions`、`of_pages_*`、`of_apply_logs` 等)。
|
||||||
|
|||||||
Reference in New Issue
Block a user