diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx
index 862568c6..644f74bc 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx
@@ -223,8 +223,9 @@ test('reconciles service identities with bound and identity-required vehicles',
expect(screen.getByText('全部协议达到当前口径')).toBeInTheDocument();
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '运行概览', level: 5 })).toBeInTheDocument();
- expect(screen.getByText('车辆总数 / 在线').closest('.v2-ops-metric')).toBeInTheDocument();
- expect(screen.getByRole('list', { name: '运行健康指标' })).toHaveClass('v2-ops-metric-rail');
+ expect(screen.getByText('车辆总数 / 在线').closest('[role="listitem"]')).toHaveClass('is-secondary');
+ expect(screen.getByRole('list', { name: '运行健康指标' })).toHaveClass('v2-workspace-metric-list');
+ expect(screen.getByText('Kafka Lag').closest('.v2-workspace-metric-context')).toHaveClass('v2-workspace-metric-context');
expect(screen.getByText('全部正常')).toBeInTheDocument();
expect(screen.getByText('运行时安全').closest('.semi-card')).toHaveClass('v2-ops-runtime');
expect(screen.getByText('协议来源就绪度').closest('.semi-card')).toHaveClass('v2-ops-sources');
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
index eea9ff4a..526a8f7f 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
@@ -13,6 +13,7 @@ import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
+import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
@@ -49,19 +50,6 @@ function number(value?: number, digits = 1) {
return value == null || !Number.isFinite(value) ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits });
}
-function OpsMetric({ label, value, detail, tone = 'neutral' }: {
- label: string;
- value: string;
- detail: string;
- tone?: 'neutral' | 'success' | 'warning' | 'danger';
-}) {
- return
- {label}
- {value}
- {detail}
- ;
-}
-
function sourceOnlineRate(online: number, total: number, onlineRate?: number) {
if (onlineRate != null && Number.isFinite(onlineRate)) return Math.max(0, Math.min(100, onlineRate));
return total > 0 ? Math.max(0, Math.min(100, (online / total) * 100)) : 0;
@@ -457,6 +445,34 @@ export default function OperationsPage() {
: healthIssueCount > 0
? `协议覆盖 ${sourceIssueCount} 项 · 链路运行 ${infrastructureIssueCount} 项`
: '链路、写入与协议覆盖均正常';
+ const healthMetrics: WorkspaceQueueMetricRailItem[] = [
+ {
+ label: '整体状态',
+ value: overallValue,
+ note: overallDetail,
+ tone: healthEvidencePending ? 'neutral' : readinessUnavailable || sourceErrorCount > 0 ? 'danger' : healthIssueCount > 0 ? 'warning' : 'success',
+ emphasis: 'primary'
+ },
+ {
+ label: '协议就绪 / 总数',
+ value: sources?.sources.length ? `${sourceHealthyCount} / ${sources.sources.length}` : readinessUnavailable ? '不可用' : '—',
+ note: readinessUnavailable ? '来源证据读取失败' : sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需要处置` : sources?.sources.length ? '全部协议达到当前口径' : '等待协议覆盖证据',
+ tone: readinessUnavailable || sourceErrorCount > 0 ? 'danger' : sourceIssueCount > 0 ? 'warning' : sources?.sources.length ? 'success' : 'neutral',
+ emphasis: 'primary'
+ },
+ {
+ label: '车辆总数 / 在线',
+ value: sources ? `${sources.totalVehicles} / ${sources.onlineVehicles}` : '—',
+ note: sources ? `已绑定 ${sources.boundVehicles} · 待绑定 ${sources.identityRequiredVehicles}` : '档案与快照并集',
+ emphasis: 'secondary'
+ },
+ {
+ label: '活跃连接',
+ value: data?.activeConnections?.toLocaleString('zh-CN') ?? '—',
+ note: '网关实时连接',
+ emphasis: 'secondary'
+ }
+ ];
const sortedLinks = useMemo(() => [...(data?.linkHealth ?? [])].sort((left, right) => {
const priority = { error: 0, warning: 1, ok: 2 } as Record;
return (priority[left.status] ?? 3) - (priority[right.status] ?? 3);
@@ -519,18 +535,17 @@ export default function OperationsPage() {
description="关键链路、基础设施与车辆覆盖的同一健康快照"
meta={{!data ? '正在读取' : healthIssueCount > 0 ? `${healthIssueCount} 项需关注` : '全部正常'}15 秒自动刷新}
/>
-
- 0 ? 'danger' : healthIssueCount > 0 ? 'warning' : 'success'} />
- 0 ? `${sourceIssueCount} 个协议需要处置` : sources?.sources.length ? '全部协议达到当前口径' : '等待协议覆盖证据'}
- tone={readinessUnavailable || sourceErrorCount > 0 ? 'danger' : sourceIssueCount > 0 ? 'warning' : sources?.sources.length ? 'success' : 'neutral'}
- />
-
-
-
-
+
+ Kafka Lag
+ {data?.kafkaLag?.toLocaleString('zh-CN') ?? '—'}
+ {data?.kafkaLag === 0 ? '消费积压已回零' : data ? '存在待消费消息' : '等待消费证据'}
+ }
+ />
0 ? 'warning' : sources ? 'ok' : 'unknown'}>{readinessUnavailable ? '证据不可用' : sources ? sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需关注` : `${sources.sources.length} 个协议正常` : '正在读取'}} />{sources ? sortedSources.length ? {sortedSources.map((source) => {
const onlineRate = sourceOnlineRate(source.online, source.total, source.onlineRate);
diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
index cc8e07c9..f97cf367 100644
--- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
+++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
@@ -322,15 +322,17 @@ describe('V2 production entry', () => {
expect(workspaceStyles).toContain('grid-template-columns: repeat(2, minmax(0, 1fr));');
expect(workspaceStyles).toContain('Short landscape operations cockpit.');
expect(workspaceStyles).toContain('grid-template-columns: minmax(196px, .42fr) minmax(0, 1fr);');
- expect(workspaceStyles).toContain('.v2-ops-workspace.is-health .v2-ops-metric-rail {');
- expect(workspaceStyles).toContain('grid-template-columns: 1.12fr repeat(4, minmax(0, 1fr));');
+ expect(workspaceStyles).toContain('.v2-ops-health-metric-rail.is-queue > .semi-card-body {');
+ expect(workspaceStyles).toContain('grid-template-columns: minmax(0, 1fr) 196px;');
expect(workspaceStyles).toContain('.v2-ops-workspace.is-diagnostic {');
expect(workspaceStyles).toContain('grid-template-columns: minmax(176px, .34fr) minmax(0, 1fr);');
expect(corePageSources.OperationsPage).toContain(' {
expect(corePageSources.AccessPage).toContain(' .semi-card-body > .v2-workspace-panel-header {
min-height: 64px;
border-bottom: 1px solid #e7edf4;
@@ -5430,26 +5315,6 @@
font-size: 10px;
}
-@media (max-width: 900px) {
- .v2-ops-metric-rail {
- display: flex;
- overflow-x: auto;
- scroll-snap-type: x proximity;
- scrollbar-width: thin;
- }
-
- .v2-ops-metric {
- min-width: 190px;
- flex: 0 0 190px;
- scroll-snap-align: start;
- }
-
- .v2-ops-metric:first-child {
- min-width: 218px;
- flex-basis: 218px;
- }
-}
-
@media (max-width: 680px) {
.v2-ops-overview > .semi-card-body > .v2-workspace-panel-header {
min-height: 58px;
@@ -5458,25 +5323,6 @@
.v2-ops-overview-meta > .semi-typography { display: none; }
- .v2-ops-metric {
- min-width: 156px;
- min-height: 96px;
- flex-basis: 156px;
- padding: 14px 13px 12px;
- }
-
- .v2-ops-metric:first-child {
- min-width: 184px;
- flex-basis: 184px;
- }
-
- .v2-ops-metric strong {
- margin: 8px 0 7px;
- font-size: 21px;
- }
-
- .v2-ops-metric span { font-size: 10px; }
-
.v2-ops-link-card {
min-height: 72px;
grid-template-columns: minmax(0, 1fr) auto;
@@ -14595,21 +14441,6 @@
height: auto;
}
-.v2-ops-metric {
- min-height: 100px;
- padding: 14px 16px 13px;
-}
-
-.v2-ops-metric::after {
- right: 16px;
- left: 16px;
-}
-
-.v2-ops-metric strong {
- margin: 8px 0 7px;
- font-size: 22px;
-}
-
.v2-ops-workspace.is-diagnostic {
display: flex;
min-height: 0;
@@ -14724,49 +14555,6 @@
min-height: 260px;
}
- .v2-ops-workspace.is-health .v2-ops-metric-rail {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- overflow: visible;
- scroll-snap-type: none;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric {
- min-width: 0;
- min-height: 78px;
- flex: none;
- border-top: 1px solid #e7edf4;
- border-left: 0;
- padding: 11px 12px 10px;
- scroll-snap-align: none;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric:first-child {
- min-width: 0;
- min-height: 88px;
- grid-column: 1 / -1;
- border-top: 0;
- padding-inline: 13px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric:nth-child(even) {
- border-right: 1px solid #e7edf4;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric strong {
- margin: 6px 0 5px;
- font-size: 19px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric:first-child strong {
- font-size: 22px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric::after {
- right: 12px;
- left: 12px;
- }
-
.v2-ops-links .v2-ops-link-card {
min-height: 58px;
grid-template-columns: minmax(0, 1fr) auto;
@@ -14809,9 +14597,6 @@
align-items: stretch;
}
- .v2-ops-metric {
- min-height: 94px;
- }
}
/*
@@ -16443,6 +16228,35 @@
color: inherit;
}
+.v2-ops-health-metric-rail.is-queue.semi-card {
+ border-bottom: 0;
+}
+
+.v2-ops-health-metric-rail.is-queue > .semi-card-body {
+ grid-template-columns: minmax(0, 1fr) 196px;
+}
+
+.v2-ops-health-metric-rail .v2-workspace-metric-list {
+ grid-template-columns: 1.25fr 1.05fr 1fr .9fr;
+}
+
+.v2-ops-health-metric-rail .v2-workspace-metric-list > span.is-primary.is-warning {
+ background: linear-gradient(145deg, #fff8ed 0%, #fffdf8 100%);
+ box-shadow: inset 0 -3px #e59a2f;
+}
+
+.v2-ops-health-metric-rail .v2-workspace-metric-list > span.is-primary.is-warning > :is(small, strong) {
+ color: #9d5d0b;
+}
+
+.v2-ops-health-kafka.is-ok strong {
+ color: #148459;
+}
+
+.v2-ops-health-kafka.is-warning strong {
+ color: #a76512;
+}
+
.v2-reconcile-metric-rail {
flex: 0 0 auto;
}
@@ -16474,6 +16288,27 @@
}
}
+@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
+ .v2-ops-health-metric-rail.is-queue > .semi-card-body {
+ min-height: 48px;
+ grid-template-columns: minmax(0, 1fr) 142px;
+ }
+
+ .v2-ops-health-metric-rail .v2-workspace-metric-list > span {
+ min-height: 48px;
+ padding-block: 4px;
+ }
+
+ .v2-ops-health-metric-rail .v2-workspace-metric-list > span > strong {
+ font-size: 15px;
+ line-height: 18px;
+ }
+
+ .v2-ops-health-metric-rail .v2-workspace-metric-list > span > em {
+ display: none;
+ }
+}
+
@media (max-width: 680px) {
.v2-workspace-metric-rail.is-queue > .semi-card-body {
min-height: 56px;
@@ -20418,52 +20253,6 @@
overflow: hidden;
}
- .v2-ops-workspace.is-health .v2-ops-metric-rail {
- display: grid;
- grid-template-columns: 1.12fr repeat(4, minmax(0, 1fr));
- overflow: hidden;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric,
- .v2-ops-workspace.is-health .v2-ops-metric:first-child {
- min-width: 0;
- min-height: 78px;
- grid-column: auto;
- border-top: 0;
- border-right: 0;
- border-left: 1px solid #e7edf4;
- padding: 8px 9px 7px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric:first-child {
- border-left: 0;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric small {
- font-size: 8px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric strong,
- .v2-ops-workspace.is-health .v2-ops-metric:first-child strong {
- margin: 6px 0 5px;
- font-size: 15px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric:first-child strong {
- font-size: 16px;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric span {
- font-size: 8px;
- line-height: 1.3;
- }
-
- .v2-ops-workspace.is-health .v2-ops-metric::after {
- right: 9px;
- left: 9px;
- height: 2px;
- }
-
.v2-ops-workspace.is-diagnostic {
display: grid;
min-height: 0;