From aededd136272199300660fd024af69f1e60f0542 Mon Sep 17 00:00:00 2001 From: imengying Date: Sun, 30 Aug 2026 10:34:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=9F=E8=BD=BDUI=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 3 +- frontend/src/components/NodeDetails.tsx | 99 ++++++++----------------- frontend/src/styles/dashboard.css | 31 +++----- 3 files changed, 42 insertions(+), 91 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 52806ea..62b539d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -127,7 +127,7 @@ function HomeView() { } function DetailView() { - const { config, goHome, liveMetrics, loading, selectedId, servers } = useApp(); + const { config, goHome, loading, selectedId, servers } = useApp(); const locale = config.locale; const selected = servers.find((server) => server.id === selectedId) ?? null; @@ -136,7 +136,6 @@ function DetailView() { {ui(locale, "正在加载节点", "Loading server")}}> { if (active) { setLatencyTasks(latency.tasks); - setLatencyPoints((current) => mergeLatencySamples( - current, - latency.points, - latencyHours, - latency.tasks.length, - )); + setLatencyPoints(mergeLatencySamples([], latency.points, latencyHours, latency.tasks.length)); } }) .catch((reason) => { if (active) { setLatencyTasks([]); setLatencyPoints([]); setLatencyError(reason instanceof Error ? reason.message : ui(locale, "延迟数据加载失败", "Unable to load latency")); } }) .finally(() => { if (active) setLatencyLoading(false); }); return () => { active = false; }; }, [server.id, latencyHours, demo, locale]); - useEffect(() => { - if (demo || !server.latency.length) return; - setLatencyPoints((current) => mergeLatencySamples( - current, - server.latency, - latencyHours, - Math.max(latencyTasks.length, server.latency.length), - )); - }, [demo, latencyHours, latencyTasks.length, server.latency]); - - useEffect(() => { - if (demo || !liveLatencyResults.length || !latencyTasks.length) return; - const definitions = new Map(latencyTasks.map((task) => [task.id, task])); - const samples = liveLatencyResults.flatMap((result): LatencySample[] => { - const task = definitions.get(result.task_id); - if (!task || !Number.isFinite(result.timestamp) || result.timestamp <= 0) return []; - return [{ - task_id: task.id, - server_id: server.id, - name: task.name, - task_type: task.task_type, - target: task.target, - port: task.port, - timestamp: result.timestamp, - latency_ms: result.latency_ms, - packet_loss: result.packet_loss, - }]; - }); - if (!samples.length) return; - setLatencyPoints((current) => mergeLatencySamples(current, samples, latencyHours, latencyTasks.length)); - }, [demo, latencyHours, latencyTasks, liveLatencyResults, server.id]); - useEffect(() => { setHiddenLatencyTaskIds((current) => { const available = new Set(latencyTasks.map((task) => task.id)); @@ -317,8 +279,8 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa () => insertLoadGaps(points, loadChartHours), [loadChartHours, points], ); - // 有时间范围时钉住整段窗口,部分数据也保持真实比例;实时档跟随数据自适应 - // (与延迟图、Komari 原版一致)。points 进 deps 是为了在数据落位后重算 + // 有时间范围时钉住整段窗口,部分数据也保持真实比例;实时档用 dataMin/dataMax + // 贴合数据(与延迟图、Komari 原版一致)。points 进 deps 是为了在数据落位后重算 // domain,慢查询时末端不会被裁掉。 const loadTimeRange = useMemo( () => (loadHours === 0 ? null : chartTimeRange(loadChartHours)), @@ -350,14 +312,15 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa [hiddenLatencyTaskIds, latencySeries], ); const latencyData = useMemo(() => { + // 共享时间轴取全部任务(含已隐藏),全部隐藏时图表仍渲染空轴 + 覆盖提示。 const timestamps = new Set(); - for (const series of visibleLatencySeries) { + for (const series of latencySeries) { for (const point of series.points) timestamps.add(point.timestamp); } return Array.from(timestamps) .sort((left, right) => left - right) .map((timestamp) => ({ timestamp, label: chartTimeLabel(timestamp, hours, locale) })); - }, [hours, locale, visibleLatencySeries]); + }, [hours, locale, latencySeries]); const latencyTimeRange = useMemo(() => chartTimeRange(latencyHours), [latencyHours, latencyPoints]); const last = data[data.length - 1]; const pingEnabled = latencyTasks.length > 0; @@ -416,53 +379,53 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa {chartType === "load" && loadLoading ?
{ui(locale, "正在读取历史数据", "Loading history")}
: chartType === "latency" && latencyLoading ?
{ui(locale, "正在读取延迟数据", "Loading latency")}
: chartType === "load" && !data.length ?
{ui(locale, "暂无历史数据", "No history")}
: chartType === "load" ? (
- 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [`${Number(value).toFixed(1)}%`, "CPU"]} /> + 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [`${Number(value).toFixed(1)}%`, "CPU"]} /> - 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> formatBytes(Number(value), 0)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "内存"]} /> + 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> formatBytes(Number(value), 0)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "内存"]} /> - 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> formatBytes(Number(value), 0)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "硬盘"]} /> + 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> formatBytes(Number(value), 0)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "硬盘"]} /> - 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> formatBytes(Number(value), 0)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value, name) => [formatSpeed(Number(value)), name === "net_out" ? "上行" : "下行"]} /> + 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /> formatBytes(Number(value), 0)} /> chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value, name) => [formatSpeed(Number(value)), name === "net_out" ? "上行" : "下行"]} />
) : (
-
- {latencySeries.map((series) =>
- {series.name} - {series.latency == null ? "--" : `${series.latency.toFixed(2)} ms`} - {series.loss == null ? "--" : `${series.loss.toFixed(2)}%`} 平均丢包 -
)} -
-
+ {latencySeries.length ?
{latencySeries.map((series) => { const hidden = hiddenLatencyTaskIds.has(series.id); + const values = `${ui(locale, "延迟", "Latency")} ${series.latency == null ? "--" : `${series.latency.toFixed(1)} ms`} · ${ui(locale, "丢包", "Loss")} ${series.loss == null ? "--" : `${series.loss.toFixed(1)}%`}`; return ; })} -
+
: null}
- {latencyData.length && visibleLatencySeries.length ? - - - chartTimeLabel(Number(value), latencyHours, locale)} /> - - chartTimeLabel(Number(value), latencyHours, locale)} formatter={(value, name) => [value == null ? "--" : `${Number(value).toFixed(1)} ms`, name]} /> - {visibleLatencySeries.map((series) => )} - - :
{visibleLatencySeries.length ? ui(locale, "暂无延迟数据", "No latency data") : ui(locale, "已隐藏全部任务", "All tasks are hidden")}
} + {latencyData.length ? <> + + + + chartTimeLabel(Number(value), latencyHours, locale)} /> + + chartTimeLabel(Number(value), latencyHours, locale)} formatter={(value, name) => [value == null ? "--" : `${Number(value).toFixed(1)} ms`, name]} /> + {visibleLatencySeries.map((series) => )} + + + {visibleLatencySeries.length ? null :
{ui(locale, "已隐藏全部延迟任务", "All latency tasks are hidden")}
} + :
{ui(locale, "暂无延迟数据", "No latency data")}
}
)} diff --git a/frontend/src/styles/dashboard.css b/frontend/src/styles/dashboard.css index fe92e1b..2516866 100644 --- a/frontend/src/styles/dashboard.css +++ b/frontend/src/styles/dashboard.css @@ -186,23 +186,18 @@ html[data-blur="on"] .node-offline-overlay { backdrop-filter: blur(2px); } .detail-chart header span { overflow: hidden; color: var(--muted-foreground); font-size: 12px; font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; } .detail-chart > div { width: 100%; height: 208px; } .latency-overview { min-width: 0; overflow: hidden; border-radius: 8px; } -.latency-summary-grid { display: grid; min-width: 100%; overflow-x: auto; border-bottom: 1px solid var(--border); } -.latency-summary { min-width: 180px; border-right: 1px solid var(--border); padding: 17px 20px; } -.latency-summary:last-child { border-right: 0; } -.latency-summary > span { display: flex; min-width: 0; align-items: center; gap: 7px; overflow: hidden; color: var(--muted-foreground); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } -.latency-summary > span i { width: 9px; height: 9px; flex: 0 0 auto; border-radius: 2px; } -.latency-summary strong { display: block; margin-top: 6px; font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: 0; } -.latency-summary small { display: block; margin-top: 5px; color: var(--muted-foreground); font-size: 12px; font-variant-numeric: tabular-nums; } -.latency-summary.hidden { opacity: .42; } -.latency-task-legend { display: flex; min-width: 0; align-items: center; gap: 6px; overflow-x: auto; border-bottom: 1px solid var(--border); padding: 10px 16px; } -.latency-task-toggle { display: inline-flex; min-width: 0; min-height: 28px; flex: 0 0 auto; align-items: center; gap: 6px; border: 1px solid transparent; border-radius: 6px; padding: 4px 8px; color: var(--muted-foreground); font-size: 11px; white-space: nowrap; } -.latency-task-toggle:hover { color: var(--foreground); background: color-mix(in srgb, var(--foreground) 6%, transparent); } +.latency-task-legend { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 6px 20px; border-bottom: 1px solid var(--border); padding: 10px 16px; } +.latency-task-toggle { display: inline-flex; min-width: 0; max-width: 100%; min-height: 28px; flex: 0 0 auto; align-items: center; gap: 7px; border: 0; border-radius: 6px; padding: 4px 6px; color: var(--foreground); font-size: 12px; white-space: nowrap; transition: opacity 150ms ease; } +.latency-task-toggle:hover { background: color-mix(in srgb, var(--foreground) 6%, transparent); } .latency-task-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; } -.latency-task-toggle i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 2px; } -.latency-task-toggle.hidden { border-color: var(--border); color: var(--muted-foreground); opacity: .58; } -.latency-task-toggle.hidden i { opacity: .38; } -.latency-line-chart { width: 100%; height: 320px; padding: 16px 18px 10px 10px; } +.latency-task-toggle i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; } +.latency-task-toggle span { min-width: 0; overflow: hidden; max-width: 160px; font-weight: 500; text-overflow: ellipsis; } +.latency-task-toggle em { flex: 0 0 auto; color: var(--muted-foreground); font-style: normal; font-variant-numeric: tabular-nums; } +.latency-task-toggle.hidden { opacity: .35; } +.latency-task-toggle.hidden:hover { opacity: .65; } +.latency-line-chart { position: relative; width: 100%; height: 288px; padding: 16px 18px 10px 10px; } .latency-empty { display: grid; width: 100%; height: 100%; place-items: center; color: var(--muted-foreground); font-size: 13px; } +.latency-empty-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--muted-foreground); font-size: 12px; pointer-events: none; } @media (min-width: 640px) { .brand strong { font-size: 18px; } @@ -245,11 +240,5 @@ html[data-blur="on"] .node-offline-overlay { backdrop-filter: blur(2px); } .range-control { width: 100%; } .range-control button { flex: 0 0 auto; } .detail-chart > div { height: 192px; } - .latency-summary { padding: 14px 16px; } - .latency-summary strong { font-size: 19px; } - .latency-summary-grid { overflow: visible; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } - .latency-summary { min-width: 0; } - .latency-summary:nth-child(2n) { border-right: 0; } - .latency-summary:nth-child(n+3) { border-top: 1px solid var(--border); } .latency-line-chart { height: 270px; padding: 12px 10px 8px 4px; } }