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} />