diff --git a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx index 64fb4774..15421625 100644 --- a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx @@ -1,4 +1,5 @@ import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; +import { IconCopy } from '@douyinfe/semi-icons'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types'; @@ -226,6 +227,44 @@ function capacityRisk(finding: string): CapacityRisk { }; } +function opsCapacityHandoffText(health: OpsHealth | null) { + const metrics = getCapacityMetrics(health); + const risks = (health?.capacityFindings ?? []).map(capacityRisk); + const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok'); + const runtime = health?.runtime; + return [ + '【车辆数据中台运维容量交接】', + `运行版本:${runtime?.platformRelease || '-'}`, + `Kafka Lag:${formatNumber(metrics.kafkaLag)}`, + `活跃连接:${formatNumber(metrics.activeConnections)}`, + `Redis 在线 Key:${formatNumber(health?.redisOnlineKeys)}`, + `NATS桥接:consumer pending ${formatNumber(metrics.bridgeConsumerPending)} / ack pending ${formatNumber(metrics.bridgeAckPending)} / batch pending ${formatNumber(metrics.bridgeBatchPendingMessages)}`, + `快速写入:consumer pending ${formatNumber(metrics.fastWriterConsumerPending)} / ack pending ${formatNumber(metrics.fastWriterAckPending)} / batch pending ${formatNumber(metrics.fastWriterBatchPending)}`, + `历史写入:batch pending ${formatNumber(metrics.historyBatchPending)} / rows pending ${formatNumber(metrics.historyRowsPending)}`, + `存储状态:TDengine ${statusText(health?.tdengineWritable)} / MySQL ${statusText(health?.mysqlWritable)}`, + `地图配置:Web JS ${runtime?.amapWebJsConfigured ? '已配置' : '未配置'} / API ${runtime?.amapApiConfigured ? '已配置' : '未配置'} / ${amapSecurityText(health)}`, + `异常链路:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}(${item.detail || '-'})`).join(';') : '正常'}`, + '', + '容量发现:', + ...(risks.length > 0 ? risks.map((risk, index) => `${index + 1}. [${risk.category}] ${risk.finding} -> ${risk.action}`) : ['暂无容量风险']), + '', + '处置顺序:', + '1. 先看 Kafka Lag 与 NATS ACK pending,确认消息是否卡在桥接或下游消费', + '2. 再看快速写入 pending,确认 Redis/TDengine 实时链路是否影响在线和实时数据', + '3. 再看历史写入 pending,确认轨迹、RAW、统计是否滞后', + '4. 最后核对 MySQL、TDengine、Redis 和地图配置,确认中台查询体验是否受影响' + ].join('\n'); +} + +async function copyText(value: string, label: string) { + try { + await navigator.clipboard.writeText(value); + Toast.success(`已复制${label}`); + } catch { + Toast.error(`复制${label}失败`); + } +} + export function OpsQuality() { const [health, setHealth] = useState(null); const [loading, setLoading] = useState(true); @@ -252,7 +291,12 @@ export function OpsQuality() { 刷新} + actions={( + + + + + )} />
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 432186dd..0eeea742 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3497,6 +3497,11 @@ test('renders notification rules as a standalone operations page', async () => { test('renders ops quality as a standalone runtime health page', async () => { window.history.replaceState(null, '', '/#/ops-quality'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { @@ -3579,6 +3584,14 @@ test('renders ops quality as a standalone runtime health page', async () => { expect(screen.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('桥接处置顺序')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined); + + fireEvent.click(screen.getByRole('button', { name: '复制容量交接' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台运维容量交接】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-ops-test')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Kafka Lag:42')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('NATS桥接:consumer pending 1,482,047 / ack pending 4,000')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('存储状态:TDengine 正常 / MySQL 异常')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 先看 Kafka Lag 与 NATS ACK pending')); }); test('copies notification text from quality priority queue', async () => {