From 1e4a11d806999a2a10c8c8552dcf3f0af805f1d8 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 20:40:29 +0800 Subject: [PATCH] feat(platform): separate ops quality as evidence layer --- .../apps/web/src/pages/OpsQuality.tsx | 57 ++++++++++++++++- .../apps/web/src/styles/global.css | 64 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 11 +++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx index ee21dde8..08c19059 100644 --- a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx @@ -75,6 +75,33 @@ type OpsActionItem = { acceptance: string; }; +const customerEvidencePaths = [ + { + title: '车辆不在线', + action: '看实时写入', + evidence: 'Redis 在线 Key、快速写入 pending、NATS ACK', + detail: '解释客户看到车辆离线时,是否是实时帧、在线 TTL 或快速写入链路中断。' + }, + { + title: '轨迹缺数据', + action: '看历史消费', + evidence: 'Kafka Lag、历史批次、历史行待写', + detail: '解释轨迹回放或历史明细缺失时,数据是否卡在 Kafka、TDengine 或批量写入。' + }, + { + title: '无法导出', + action: '看存储/API', + evidence: 'TDengine、MySQL、请求超时、运行版本', + detail: '解释客户导出失败、查询慢或字段缺失时,核心存储和 API 是否可用。' + }, + { + title: '地图不可用', + action: '看高德配置', + evidence: 'Web JS Key、服务端 API Key、安全代理', + detail: '解释实时地图、轨迹回放和地理能力不可用时,地图配置是否完整且安全。' + } +]; + const emptyCapacityMetrics: CapacityMetrics = { activeConnections: 0, kafkaLag: 0, @@ -457,8 +484,8 @@ export function OpsQuality() { return (
@@ -468,6 +495,32 @@ export function OpsQuality() { )} /> + +
+
+ 客户问题解释路径 + 运维证据只在客户问题需要解释时打开,默认不参与车辆服务主流程。 +
+ 证据层 +
+
+ {customerEvidencePaths.map((item) => ( + + ))} +
+
+
{formatNumber(health?.kafkaLag)}
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index cf0db422..fb191b0a 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -5624,6 +5624,69 @@ button.vp-realtime-command-item:focus-visible { line-height: 32px; } +.vp-ops-evidence-layer-card { + margin-bottom: 16px; +} + +.vp-ops-evidence-layer-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.vp-ops-evidence-path-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-ops-evidence-path-item { + min-height: 168px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + color: inherit; + text-align: left; + cursor: pointer; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-ops-evidence-path-item:hover { + border-color: var(--vp-primary); + box-shadow: 0 10px 24px rgba(21, 101, 216, 0.08); +} + +.vp-ops-evidence-path-title { + color: var(--vp-text); + font-size: 15px; + font-weight: 700; + line-height: 22px; +} + +.vp-ops-evidence-path-action { + color: var(--vp-primary); + font-size: 20px; + font-weight: 700; + line-height: 28px; +} + +.vp-ops-evidence-path-evidence { + color: var(--vp-text); + font-size: 13px; + line-height: 20px; +} + +.vp-ops-evidence-path-detail { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + .vp-risk-runbook { margin-top: 12px; padding: 12px; @@ -9865,6 +9928,7 @@ button.vp-realtime-command-item:focus-visible { .vp-notification-reachability-grid, .vp-sla-summary, .vp-risk-grid, + .vp-ops-evidence-path-grid, .vp-ops-action-grid, .vp-alert-ops-grid, .vp-stat-workspace, diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index db35f807..cc78c585 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4391,7 +4391,7 @@ test('renders notification rules as a standalone operations page', async () => { expect(window.location.hash).toBe('#/detail?keyword=VIN-RULES-001'); }); -test('renders ops quality as a standalone runtime health page', async () => { +test('renders ops quality as a separated evidence layer for customer vehicle service', async () => { window.history.replaceState(null, '', '/#/ops-quality'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { @@ -4512,7 +4512,14 @@ test('renders ops quality as a standalone runtime health page', async () => { render(); - expect(await screen.findByRole('heading', { name: '运维质量' })).toBeInTheDocument(); + expect(await screen.findByRole('heading', { name: '运维证据层' })).toBeInTheDocument(); + expect(screen.getByText('客户主流程外的链路、容量、存储和地图配置证据,用来解释车辆实时、轨迹、统计、导出或告警为何不可用。')).toBeInTheDocument(); + expect(screen.getByText('客户问题解释路径')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户问题解释路径 车辆不在线 看实时写入' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户问题解释路径 轨迹缺数据 看历史消费' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户问题解释路径 无法导出 看存储/API' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户问题解释路径 地图不可用 看高德配置' })).toBeInTheDocument(); + expect(screen.getByText('运维证据只在客户问题需要解释时打开,默认不参与车辆服务主流程。')).toBeInTheDocument(); expect(screen.getByText('platform-ops-test')).toBeInTheDocument(); expect(screen.getAllByText('42').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('1,234').length).toBeGreaterThanOrEqual(1);