From 7ed5d7620b4d988459470514e3e920f5b908a89e Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 04:38:09 +0800 Subject: [PATCH] feat(platform): align quality issues with vehicle service --- .../apps/web/src/domain/qualityIssue.test.ts | 29 ++++++++ .../apps/web/src/domain/qualityIssue.ts | 28 +++++++ .../apps/web/src/pages/Quality.tsx | 13 ++-- .../apps/web/src/test/App.test.tsx | 73 +++++++++++++++++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 vehicle-data-platform/apps/web/src/domain/qualityIssue.test.ts create mode 100644 vehicle-data-platform/apps/web/src/domain/qualityIssue.ts diff --git a/vehicle-data-platform/apps/web/src/domain/qualityIssue.test.ts b/vehicle-data-platform/apps/web/src/domain/qualityIssue.test.ts new file mode 100644 index 00000000..73683476 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/domain/qualityIssue.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'vitest'; +import { qualityIssueLabel, qualityProtocolLabel, qualityProtocolOptions } from './qualityIssue'; + +describe('qualityIssueLabel', () => { + test('maps no-source issues to a vehicle-service label', () => { + expect(qualityIssueLabel('NO_SOURCE')).toBe('暂无数据来源'); + }); + + test('keeps unknown issue types visible', () => { + expect(qualityIssueLabel('CUSTOM_GAP')).toBe('CUSTOM_GAP'); + }); +}); + +describe('qualityProtocolLabel', () => { + test('labels vehicle-level quality issues as vehicle service', () => { + expect(qualityProtocolLabel('VEHICLE_SERVICE')).toBe('车辆服务'); + }); +}); + +describe('qualityProtocolOptions', () => { + test('includes vehicle service and all ingestion sources', () => { + expect(qualityProtocolOptions.map((item) => item.value)).toEqual([ + 'VEHICLE_SERVICE', + 'GB32960', + 'JT808', + 'YUTONG_MQTT' + ]); + }); +}); diff --git a/vehicle-data-platform/apps/web/src/domain/qualityIssue.ts b/vehicle-data-platform/apps/web/src/domain/qualityIssue.ts new file mode 100644 index 00000000..3ca106c7 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/domain/qualityIssue.ts @@ -0,0 +1,28 @@ +const issueLabels: Record = { + NO_SOURCE: '暂无数据来源', + VIN_MISSING: 'VIN 缺失', + LINK_GAP: '链路间断', + FIELD_MISSING: '字段缺失' +}; + +const protocolLabels: Record = { + VEHICLE_SERVICE: '车辆服务', + GB32960: 'GB32960', + JT808: 'JT808', + YUTONG_MQTT: '宇通 MQTT' +}; + +export const qualityProtocolOptions = [ + { value: 'VEHICLE_SERVICE', label: protocolLabels.VEHICLE_SERVICE }, + { value: 'GB32960', label: protocolLabels.GB32960 }, + { value: 'JT808', label: protocolLabels.JT808 }, + { value: 'YUTONG_MQTT', label: protocolLabels.YUTONG_MQTT } +]; + +export function qualityIssueLabel(issueType: string) { + return issueLabels[issueType] ?? issueType; +} + +export function qualityProtocolLabel(protocol: string) { + return protocolLabels[protocol] ?? protocol; +} diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 44383c84..c1814f65 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { OpsHealth, QualitySummary, QualityIssueRow } from '../api/types'; import { PageHeader } from '../components/PageHeader'; +import { qualityIssueLabel, qualityProtocolLabel, qualityProtocolOptions } from '../domain/qualityIssue'; import { qualityIssueVehicleLookup } from '../domain/vehicleLookup'; const statusColor: Record = { @@ -120,7 +121,7 @@ export function Quality({ { label: '错误 / 警告', value: `${summary.errorCount}/${summary.warningCount}` }, { label: '主要问题', - value: summary.issueTypes.length > 0 ? `${summary.issueTypes[0].name} ${summary.issueTypes[0].count}` : '-' + value: summary.issueTypes.length > 0 ? `${qualityIssueLabel(summary.issueTypes[0].name)} ${summary.issueTypes[0].count}` : '-' } ].map((item) => ( @@ -146,9 +147,11 @@ export function Quality({ loadSummary(nextFilters); loadIssues(nextFilters, 1, pagination.pageSize); }} style={{ marginBottom: 12 }}> - + - JT808 + {qualityProtocolOptions.map((item) => ( + {item.label} + ))} @@ -176,8 +179,8 @@ export function Quality({ { title: '车牌', dataIndex: 'plate' }, { title: '手机号', dataIndex: 'phone' }, { title: '来源地址', dataIndex: 'sourceEndpoint' }, - { title: '数据来源', dataIndex: 'protocol' }, - { title: '问题', dataIndex: 'issueType' }, + { title: '数据来源', render: (_: unknown, row: QualityIssueRow) => qualityProtocolLabel(row.protocol) }, + { title: '问题', render: (_: unknown, row: QualityIssueRow) => qualityIssueLabel(row.issueType) }, { title: '级别', render: (_: unknown, row: QualityIssueRow) => {row.severity} }, { title: '最后时间', dataIndex: 'lastSeen' }, { title: '说明', dataIndex: 'detail' }, 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 51afb0c0..ba7fcef1 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -656,6 +656,79 @@ test('opens dashboard coverage from service status distribution', async () => { }); }); +test('renders quality issues as vehicle-service governance labels', async () => { + window.history.replaceState(null, '', '/#/quality'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/summary')) { + return { + ok: true, + json: async () => ({ + data: { + issueVehicleCount: 1, + issueRecordCount: 1, + errorCount: 0, + warningCount: 1, + protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], + issueTypes: [{ name: 'NO_SOURCE', count: 1 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/issues')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-NO-SOURCE-001', + plate: '粤A无源1', + phone: '13300000002', + sourceEndpoint: 'vehicle_identity_binding', + protocol: 'VEHICLE_SERVICE', + issueType: 'NO_SOURCE', + severity: 'warning', + lastSeen: '', + detail: '车辆已绑定,但暂无 32960、808 或 MQTT 数据来源' + }], + total: 1, + limit: 20, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-NO-SOURCE-001')).toBeInTheDocument(); + expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('车辆服务').length).toBeGreaterThanOrEqual(1); +}); + test('opens vehicle detail from shareable hash', async () => { window.history.replaceState(null, '', '/#/detail?keyword=%E7%B2%A4AG18312'); vi.spyOn(globalThis, 'fetch').mockResolvedValue({