mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
前端优化
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||||
import type { EChartsCoreOption } from 'echarts/core';
|
import type {EChartsCoreOption} from 'echarts/core';
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { MapChart, ScatterChart } from 'echarts/charts';
|
import {MapChart, ScatterChart} from 'echarts/charts';
|
||||||
import { GeoComponent, TooltipComponent } from 'echarts/components';
|
import {GeoComponent, TooltipComponent} from 'echarts/components';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import {CanvasRenderer} from 'echarts/renderers';
|
||||||
import {useTheme} from 'next-themes';
|
import {useTheme} from 'next-themes';
|
||||||
import {useRouter} from 'next/navigation';
|
import {useRouter} from 'next/navigation';
|
||||||
import {useEffect, useMemo, useRef, useState} from 'react';
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
||||||
@@ -13,10 +13,7 @@ import {useEffect, useMemo, useRef, useState} from 'react';
|
|||||||
import {EmptyState} from '@/components/layout/empty';
|
import {EmptyState} from '@/components/layout/empty';
|
||||||
import type {DashboardNodeHealth, DistributionItem} from '@/lib/services/openflare';
|
import type {DashboardNodeHealth, DistributionItem} from '@/lib/services/openflare';
|
||||||
|
|
||||||
import {
|
import {getNodeStatusLabel, getOpenrestyStatusLabel,} from '../../nodes/components/node-utils';
|
||||||
getNodeStatusLabel,
|
|
||||||
getOpenrestyStatusLabel,
|
|
||||||
} from '../../nodes/components/node-utils';
|
|
||||||
import worldGeoJson from './data/world-geo.json';
|
import worldGeoJson from './data/world-geo.json';
|
||||||
|
|
||||||
echarts.use([
|
echarts.use([
|
||||||
@@ -100,9 +97,9 @@ type CountryRegionDatum = {
|
|||||||
let worldMapRegistrationAttempted = false;
|
let worldMapRegistrationAttempted = false;
|
||||||
let worldMapRegistrationSucceeded = false;
|
let worldMapRegistrationSucceeded = false;
|
||||||
|
|
||||||
const baseWorldMapLayoutSizePercent = 108;
|
const baseWorldMapLayoutSizePercent = 118;
|
||||||
const baseWorldMapZoom = 1;
|
const baseWorldMapZoom = 1;
|
||||||
const worldMapAspectRatio = 2 / 3;
|
const worldMapAspectRatio = 5 / 3;
|
||||||
|
|
||||||
function buildNodeDetailHref(id?: number | null) {
|
function buildNodeDetailHref(id?: number | null) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|||||||
@@ -1,26 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import {useEffect, useRef, useState, type ComponentType} from 'react';
|
import {type ComponentType, useEffect, useRef, useState} from 'react';
|
||||||
import {
|
import {Activity, Cpu, Globe2, HardDrive, MemoryStick, Network, Server, ShieldCheck,} from 'lucide-react';
|
||||||
Activity,
|
|
||||||
Cpu,
|
|
||||||
Globe2,
|
|
||||||
HardDrive,
|
|
||||||
MemoryStick,
|
|
||||||
Server,
|
|
||||||
ShieldCheck,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
import {EmptyState} from '@/components/layout/empty';
|
import {EmptyState} from '@/components/layout/empty';
|
||||||
import {Badge} from '@/components/ui/badge';
|
import {Badge} from '@/components/ui/badge';
|
||||||
import {
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import {Progress} from '@/components/ui/progress';
|
import {Progress} from '@/components/ui/progress';
|
||||||
import type {
|
import type {
|
||||||
DashboardCapacity,
|
DashboardCapacity,
|
||||||
@@ -45,7 +31,7 @@ const LEGEND_ITEMS = [
|
|||||||
{dot: 'bg-destructive', label: '异常'},
|
{dot: 'bg-destructive', label: '异常'},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function CompactMetric({
|
function SummaryMetric({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
hint,
|
hint,
|
||||||
@@ -63,25 +49,57 @@ function CompactMetric({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'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',
|
'flex min-w-0 flex-col gap-2 rounded-lg border border-dashed bg-muted/15 p-3',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-1.5">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="truncate text-[10px] text-muted-foreground">{label}</span>
|
<span className="truncate text-xs text-muted-foreground">{label}</span>
|
||||||
<Icon className="size-3 shrink-0 text-primary/60" />
|
<Icon className="size-3.5 shrink-0 text-primary/60" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-semibold tabular-nums leading-none tracking-tight">
|
<span className="text-xl font-semibold tabular-nums leading-none tracking-tight">
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
{typeof progress === 'number' ? (
|
{typeof progress === 'number' ? (
|
||||||
<Progress value={progress} className="h-0.5" />
|
<Progress value={progress} className="h-1" />
|
||||||
) : null}
|
) : null}
|
||||||
<span className="truncate text-[10px] text-muted-foreground">{hint}</span>
|
<span className="truncate text-[10px] text-muted-foreground">{hint}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DetailMetric({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
hint,
|
||||||
|
icon: Icon,
|
||||||
|
progress,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
hint: string;
|
||||||
|
icon: ComponentType<{className?: string}>;
|
||||||
|
progress?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 flex-col gap-1 rounded-lg border border-dashed bg-background/60 px-3 py-2.5">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-[11px] text-muted-foreground">{label}</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold tabular-nums leading-none tracking-tight">
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
{typeof progress === 'number' ? (
|
||||||
|
<Progress value={progress} className="h-1" />
|
||||||
|
) : null}
|
||||||
|
<p className="truncate text-[10px] text-muted-foreground">{hint}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function WorldStage({
|
export function WorldStage({
|
||||||
summary,
|
summary,
|
||||||
traffic,
|
traffic,
|
||||||
@@ -149,7 +167,7 @@ export function WorldStage({
|
|||||||
? '节点坐标'
|
? '节点坐标'
|
||||||
: '覆盖信号';
|
: '覆盖信号';
|
||||||
|
|
||||||
const metrics = [
|
const headlineMetrics = [
|
||||||
{
|
{
|
||||||
label: '在线覆盖',
|
label: '在线覆盖',
|
||||||
value: formatPercent(onlineRate),
|
value: formatPercent(onlineRate),
|
||||||
@@ -165,11 +183,23 @@ export function WorldStage({
|
|||||||
progress: healthyRate,
|
progress: healthyRate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '窗口请求',
|
label: '最近窗口请求',
|
||||||
value: formatCompactNumber(traffic.request_count),
|
value: formatCompactNumber(traffic.request_count),
|
||||||
hint: `QPS ${traffic.estimated_qps.toFixed(1)}`,
|
hint: `QPS ${traffic.estimated_qps.toFixed(1)} · ${traffic.reported_nodes} 个节点上报`,
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '节点坐标',
|
||||||
|
value: formatCompactNumber(geoConfiguredNodes),
|
||||||
|
hint:
|
||||||
|
geoConfiguredNodes > 0
|
||||||
|
? `${sourceCountries.length} 个来源国家`
|
||||||
|
: '未配置时使用预设落点',
|
||||||
|
icon: Network,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const detailMetrics = [
|
||||||
{
|
{
|
||||||
label: '平均 CPU',
|
label: '平均 CPU',
|
||||||
value: formatPercent(capacity.average_cpu_usage_percent),
|
value: formatPercent(capacity.average_cpu_usage_percent),
|
||||||
@@ -193,9 +223,9 @@ export function WorldStage({
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-dashed shadow-none">
|
<Card className="overflow-hidden border-dashed shadow-none">
|
||||||
<CardHeader className="gap-2 space-y-0 pb-3">
|
<CardHeader className="gap-3 space-y-0 pb-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-x-4 gap-y-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<CardTitle className="flex items-center gap-1.5 text-sm font-semibold">
|
<CardTitle className="flex items-center gap-1.5 text-sm font-semibold">
|
||||||
<Globe2 className="size-4 shrink-0 text-primary" />
|
<Globe2 className="size-4 shrink-0 text-primary" />
|
||||||
@@ -207,11 +237,11 @@ export function WorldStage({
|
|||||||
{mapModeLabel}
|
{mapModeLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="mt-1 text-xs">
|
||||||
节点分布与来源热度
|
汇总节点分布、来源国家热度与最近窗口运行状态。
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-full border border-dashed bg-muted/15 px-3 py-1.5">
|
||||||
{LEGEND_ITEMS.map((item) => (
|
{LEGEND_ITEMS.map((item) => (
|
||||||
<span
|
<span
|
||||||
key={item.label}
|
key={item.label}
|
||||||
@@ -225,12 +255,18 @@ export function WorldStage({
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="pt-0">
|
<CardContent className="flex flex-col gap-4 pt-0">
|
||||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_12.5rem] lg:gap-4 lg:items-center">
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<div className="flex min-w-0 items-center justify-center">
|
{headlineMetrics.map((metric) => (
|
||||||
|
<SummaryMetric key={metric.label} {...metric} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_18rem] xl:items-stretch">
|
||||||
|
<div className="flex min-w-0 items-center justify-center rounded-lg border border-dashed bg-muted/10 p-3">
|
||||||
<div
|
<div
|
||||||
ref={mapViewportRef}
|
ref={mapViewportRef}
|
||||||
className="relative aspect-[2/3] h-[200px] w-auto overflow-hidden rounded-lg border border-dashed bg-muted/20 sm:h-[228px] lg:h-[252px]"
|
className="relative aspect-[5/3] min-h-[260px] w-full max-w-[720px] overflow-hidden rounded-md bg-background/60 sm:min-h-0"
|
||||||
>
|
>
|
||||||
{shouldRenderMap ? (
|
{shouldRenderMap ? (
|
||||||
<WorldStageMap nodes={nodes} sourceCountries={sourceCountries} />
|
<WorldStageMap nodes={nodes} sourceCountries={sourceCountries} />
|
||||||
@@ -254,9 +290,9 @@ export function WorldStage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-1">
|
||||||
{metrics.map((metric) => (
|
{detailMetrics.map((metric) => (
|
||||||
<CompactMetric key={metric.label} {...metric} />
|
<DetailMetric key={metric.label} {...metric} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {usePathname, useRouter, useSearchParams} from 'next/navigation';
|
import {usePathname, useRouter, useSearchParams} from 'next/navigation';
|
||||||
import {useCallback, useMemo, type ReactNode} from 'react';
|
import {type ReactNode, useCallback, useMemo} from 'react';
|
||||||
import type {LucideIcon} from 'lucide-react';
|
import type {LucideIcon} from 'lucide-react';
|
||||||
import {ArrowLeft} from 'lucide-react';
|
import {ArrowLeft} from 'lucide-react';
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ export type NodeDetailTabId = 'overview' | 'dashboard' | 'manage';
|
|||||||
export type NodeDetailTabConfig = {
|
export type NodeDetailTabConfig = {
|
||||||
id: NodeDetailTabId;
|
id: NodeDetailTabId;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,17 +32,14 @@ const TAB_CONFIGS: NodeDetailTabConfig[] = [
|
|||||||
{
|
{
|
||||||
id: 'overview',
|
id: 'overview',
|
||||||
label: '概览',
|
label: '概览',
|
||||||
description: '快速查看节点身份、运行状态与关键指标。',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dashboard',
|
id: 'dashboard',
|
||||||
label: '数据看板',
|
label: '数据看板',
|
||||||
description: '系统画像、资源快照、流量趋势与健康事件时间线。',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'manage',
|
id: 'manage',
|
||||||
label: '配置与部署',
|
label: '配置与部署',
|
||||||
description: '版本同步、部署命令与节点运维配置。',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -150,7 +146,7 @@ export function NodeDetailShell({
|
|||||||
}}
|
}}
|
||||||
className="w-full gap-0"
|
className="w-full gap-0"
|
||||||
>
|
>
|
||||||
<div className="space-y-3 border-b pb-1">
|
<div className="space-y-3 pb-1">
|
||||||
<TabsList variant="line" className="h-auto w-full justify-start gap-6 bg-transparent p-0">
|
<TabsList variant="line" className="h-auto w-full justify-start gap-6 bg-transparent p-0">
|
||||||
{TAB_CONFIGS.map((tab) => (
|
{TAB_CONFIGS.map((tab) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
@@ -165,7 +161,6 @@ export function NodeDetailShell({
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<p className="text-sm text-muted-foreground">{activeTabMeta.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="overview" className="mt-6 outline-none">
|
<TabsContent value="overview" className="mt-6 outline-none">
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ 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,指标改为侧栏密集单元格,图例改为行内色点标注。
|
- Wavelet 全球态势板布局重构:关键指标前置为摘要栅格,地图与容量诊断并排展示,移动端自动堆叠。
|
||||||
- 修复 Wavelet 全球态势板 ECharts 地图在紧凑布局下容器尺寸为 0 导致无法渲染的问题。
|
- 修复 Wavelet 全球态势板 ECharts 地图在紧凑布局下容器尺寸为 0 导致无法渲染的问题。
|
||||||
- Wavelet 全球态势板地图容器调整为约 2:3(宽:高)比例,避免世界地图横向过度拉伸。
|
- Wavelet 全球态势板地图容器调整为横向看板比例,避免世界地图被压缩成窄竖卡片。
|
||||||
|
|
||||||
- 将 `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