From 85c2043f2fa702196a1f69bd8620ec8b8ed0a12d Mon Sep 17 00:00:00 2001 From: JustMe Date: Sat, 10 Oct 2026 14:17:43 +0800 Subject: [PATCH] feat(display): explain QUIC state and compact panels [CI] --- web/display/src/LineStrips.tsx | 30 +++++++----- web/display/src/display.css | 74 +++++++++++++++++++++++------- web/display/src/pages/Line.tsx | 27 +++-------- web/display/src/pages/Overview.tsx | 27 +++++++---- 4 files changed, 101 insertions(+), 57 deletions(-) diff --git a/web/display/src/LineStrips.tsx b/web/display/src/LineStrips.tsx index 7718597..add8ee5 100644 --- a/web/display/src/LineStrips.tsx +++ b/web/display/src/LineStrips.tsx @@ -6,9 +6,9 @@ import { useQuery } from '@shared/hooks' /** 总览卡片可以看的时长,以及每一格的长度(格数约 36~48,太多就挤了)。 */ export const OVERVIEW_RANGES = [ - { value: '3h', label: '3 小时', hours: 3, step: '5m', stepMin: 5 }, - { value: '6h', label: '6 小时', hours: 6, step: '10m', stepMin: 10 }, - { value: '12h', label: '12 小时', hours: 12, step: '15m', stepMin: 15 }, + { value: '3h', label: '近 3 小时', hours: 3, step: '5m', stepMin: 5 }, + { value: '6h', label: '近 6 小时', hours: 6, step: '10m', stepMin: 10 }, + { value: '12h', label: '近 12 小时', hours: 12, step: '15m', stepMin: 15 }, ] as const export type OverviewRange = (typeof OVERVIEW_RANGES)[number]['value'] @@ -68,32 +68,38 @@ export function LineStrips({ lineId, range, m }: Props) {
QUIC{fmtMs(m['rtt.p50'])}悬停看 p95 / p99
-
- 去程 +
+
+ 去程 + {upText} +
{(s ? lossUp : Array.from({ length: common.cells }, () => null)).map((value, i) => ( ))}
- {upText}
-
- 回程 +
+
+ 回程 + {downText} +
{(s ? lossDown : Array.from({ length: common.cells }, () => null)).map((value, i) => ( ))}
- {downText}
-
- TCP{fmtMs(m['tcp.rtt.p50'])} +
+
+ TCP{fmtMs(m['tcp.rtt.p50'])} + {retransText} +
{(s ? retrans : Array.from({ length: common.cells }, () => null)).map((value, i) => ( ))}
- {retransText}
diff --git a/web/display/src/display.css b/web/display/src/display.css index 037d085..0a0dc76 100644 --- a/web/display/src/display.css +++ b/web/display/src/display.css @@ -116,6 +116,29 @@ margin-top: 8px; } +.line-loading-state, +.line-empty-state { + display: flex; + min-height: 58px; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 16px; + border: 1px solid var(--glass-ring); + border-radius: var(--r-card); + background: color-mix(in srgb, var(--glass-bg) 75%, transparent); + color: var(--app-ink-muted); + font-size: 13px; +} + +.line-loading-state .loading-bar { + height: 2px; +} + +.line-empty-state { + color: var(--app-ink-faint); +} + .line-page .faint, .line-page .chart-hint, .line-page .chart-section h2, @@ -1359,15 +1382,15 @@ } .line-mini-quic-head > span, -.line-mini-direction-row > span:first-child, -.line-mini-tcp-row > span:first-child { +.line-mini-direction-head > span:first-child, +.line-mini-tcp-head > span:first-child { color: var(--app-ink-muted); font-size: 10px; font-weight: 600; } .line-mini-quic-head > strong, -.line-mini-tcp-row > strong { +.line-mini-tcp-head > strong { color: var(--app-ink); font-size: 14px; font-weight: 700; @@ -1381,32 +1404,40 @@ white-space: nowrap; } -.line-mini-direction-row { +.line-mini-direction, +.line-mini-tcp { display: grid; - grid-template-columns: 34px minmax(0, 1fr) 66px; - align-items: center; - gap: 5px; + gap: 3px; min-width: 0; } -.line-mini-direction-row > span:first-child, -.line-mini-tcp-row > span:first-child { +.line-mini-direction-head, +.line-mini-tcp-head { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + line-height: 1.25; +} + +.line-mini-direction-head > span:first-child, +.line-mini-tcp-head > span:first-child { font-weight: 500; white-space: nowrap; } -.line-mini-tcp-row { - display: grid; - grid-template-columns: 34px 46px minmax(0, 1fr) max-content; - align-items: center; - gap: 5px; - min-width: 0; +.line-mini-direction-head .line-mini-badge, +.line-mini-tcp-head .line-mini-badge { + margin-left: auto; +} + +.line-mini-tcp { margin-top: 2px; padding-top: 5px; border-top: 1px solid var(--app-outline-soft); } -.line-mini-tcp-row > strong { +.line-mini-tcp-head > strong { font-size: 12px; } @@ -1693,6 +1724,7 @@ .agent-head .flag { flex: none; width: 24px; + max-width: 24px; height: 18px; border-radius: 3px; object-fit: cover; @@ -1801,6 +1833,16 @@ background: color-mix(in srgb, var(--app-ink) 7%, transparent); } +.agent-card .line-link.disconnected, +.agent-card .line-link.disconnected:hover { + filter: grayscale(1); + opacity: 0.62; +} + +.line-range.disconnected-label { + color: var(--app-ink-faint); +} + /* 线路详情、对比等页面的卡片和指标块:同样的亚克力,一层压一层 */ .card:not(.agent-card), diff --git a/web/display/src/pages/Line.tsx b/web/display/src/pages/Line.tsx index cb51dfc..d9d9d50 100644 --- a/web/display/src/pages/Line.tsx +++ b/web/display/src/pages/Line.tsx @@ -286,29 +286,16 @@ export function Line() { - {!s && (loadingNow || !line.error) && !series.error ? ( - // 还没有数据:先摆出占位,每块上面有进度条 -
-
- {Array.from({ length: 12 }, (_, i) => ( -
- -
- ))} -
-
- -
-
- {[0, 1].map((i) => ( -
- -
- ))} -
+ {!s && loadingNow && !line.error && !series.error ? ( +
+ + + 正在加载线路数据…
) : null} + {!s && !loadingNow && !series.error && line.data ?
所选时间段暂无测量数据。
: null} + {s ? (
diff --git a/web/display/src/pages/Overview.tsx b/web/display/src/pages/Overview.tsx index e29457d..b1143fb 100644 --- a/web/display/src/pages/Overview.tsx +++ b/web/display/src/pages/Overview.tsx @@ -141,8 +141,18 @@ function NodeCard({ node, labels, infos }: { node: HostItem; labels: string[]; i function LineCard({ l, range, infos }: { l: OverviewLineDto; range: OverviewRange; infos: Map }) { const probeInfo = infos.get(l.probe_id) const agentInfo = infos.get(l.agent_id) + const rangeLabel = OVERVIEW_RANGES.find((r) => r.value === range)?.label ?? range + const connectionIssue = !l.online + ? 'Agent 离线' + : l.quic_health?.state === 'retry_wait' + ? 'QUIC 拨号失败' + : !l.probe_connected + ? '测量通道未建立' + : l.quic_health && !l.quic_health.connected + ? 'QUIC 正在连接' + : null return ( - +
{probeInfo?.country ? : null} @@ -152,13 +162,12 @@ function LineCard({ l, range, infos }: { l: OverviewLineDto; range: OverviewRang {agentInfo?.country ? : null} {l.name} - {!l.probe_connected && l.online ? ( - - 测量通道未建立 - - ) : ( - {OVERVIEW_RANGES.find((r) => r.value === range)?.label ?? range} - )} + + {connectionIssue ? `${connectionIssue} · ${rangeLabel}` : rangeLabel} +
@@ -254,7 +263,7 @@ export function Overview() { { const next = new URLSearchParams(params); next.set('ip_version', v); setCarrier(''); setParams(next, { replace: true }) }} /> {groups.length > 1 ? options={[{ value: '', label: '全部' }, ...groups.map((g) => ({ value: g.key, label: g.name }))]} value={carrier} onChange={setCarrier} /> : null} - options={OVERVIEW_RANGES.map((r) => ({ value: r.value, label: `近 ${r.label}` }))} value={range} onChange={setRange} /> + options={OVERVIEW_RANGES.map((r) => ({ value: r.value, label: r.label }))} value={range} onChange={setRange} />