diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx index ec8ac8c4..d5711957 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import { MemoryRouter } from 'react-router-dom'; import type { AccessVehicleRow, Page } from '../../api/types'; @@ -55,3 +55,24 @@ test('removes old access rows immediately when the vehicle filter scope changes' expect(await screen.findByText('新接入车牌')).toBeInTheDocument(); await waitFor(() => expect(screen.queryByText('正在更新车辆接入状态…')).not.toBeInTheDocument()); }); + +test('reports and independently retries unresolved identity and threshold failures', async () => { + prepareBaseData(); + mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 }); + mocks.accessUnresolvedIdentities.mockRejectedValueOnce(new Error('待绑定身份服务超时')).mockResolvedValueOnce({ items: [{ id: 'identity-1', identifierMasked: '138****0001', protocol: 'JT808', plate: '粤A待核', latestSeenAt: '2026-07-16T04:00:00Z', recommendedAction: '核对 VIN' }], total: 1, limit: 20, offset: 0 }); + mocks.accessThresholds.mockRejectedValueOnce(new Error('阈值配置读取失败')).mockResolvedValueOnce({ version: 1, defaultThresholdSec: 300, delayThresholdSec: 60, longOfflineSec: 86_400, protocols: [], updatedBy: 'test', updatedAt: '2026-07-16T04:00:00Z', audit: [] }); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + + const identityError = await screen.findByText('待绑定身份服务超时'); + const thresholdError = await screen.findByText('阈值配置读取失败'); + fireEvent.click(within(identityError.closest('[role="alert"]')!).getByRole('button', { name: /重试/ })); + fireEvent.click(within(thresholdError.closest('[role="alert"]')!).getByRole('button', { name: /重试/ })); + + expect(await screen.findByText('另有 1 条来源身份待绑定')).toBeInTheDocument(); + expect(await screen.findByText('在线判定阈值 · v1')).toBeInTheDocument(); + expect(screen.queryByText('待绑定身份服务超时')).not.toBeInTheDocument(); + expect(screen.queryByText('阈值配置读取失败')).not.toBeInTheDocument(); + expect(mocks.accessUnresolvedIdentities).toHaveBeenCalledTimes(2); + expect(mocks.accessThresholds).toHaveBeenCalledTimes(2); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx index 1fa5a371..769189eb 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -112,9 +112,10 @@ export default function AccessPage() { const apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); }; const submit = (event: FormEvent) => { event.preventDefault(); apply(draft); }; const page = Math.floor(offset / limit) + 1; const totalPages = Math.max(1, Math.ceil((vehiclesQuery.data?.total ?? 0) / limit)); const summary = summaryQuery.data; + const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), unresolvedQuery.refetch(), thresholdQuery.refetch()]); return
-

车辆接入管理

以主车辆为对象,对照应接协议、实际接入和各协议最新上报时间

数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
+

车辆接入管理

以主车辆为对象,对照应接协议、实际接入和各协议最新上报时间

数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
{summaryQuery.isError ? summaryQuery.refetch()} /> : null}
{[ @@ -122,7 +123,9 @@ export default function AccessPage() { ].map(([label, value, tone, connectionState]) => )}
{vehiclesQuery.isError ? vehiclesQuery.refetch()} /> : null}
车辆协议接入差异优先展示应接与实接差异;时间为各协议最后接收时间
{PROTOCOLS.map((item) => )}{rows.map((row) => setSelectedVIN(row.vin)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') setSelectedVIN(row.vin); }}>{PROTOCOLS.map((protocol) => )})}
车辆品牌 / 车型应接协议{item}综合状态
{row.plate || '未绑定车牌'}{row.vin}{row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'}{row.expectedProtocols.length} 项{row.expectedProtocols.join(' / ')}
{vehiclesQuery.isFetching ?
正在更新车辆接入状态…
: null}{!vehiclesQuery.isFetching && !rows.length ?
当前筛选条件没有车辆
: null}
第 {page} / {totalPages} 页,共 {(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆
{selected ? setSelectedVIN('')} /> : null}
+ {unresolvedQuery.isError ? unresolvedQuery.refetch()} /> : null} + {thresholdQuery.isError ? thresholdQuery.refetch()} /> : null} thresholdDraft && updateThreshold.mutate(thresholdDraft)} />
; } 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 1a70e32e..fd2f37c9 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 @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { cleanup, render, screen } from '@testing-library/react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import OperationsPage from './OperationsPage'; @@ -26,3 +26,25 @@ test('reconciles service identities with bound and identity-required vehicles', expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument(); expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument(); }); + +test('keeps health evidence visible when source readiness fails and retries that section', async () => { + mocks.opsHealth.mockResolvedValue({ + linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, + tdengineWritable: true, mysqlWritable: true, + runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } + }); + mocks.sourceReadiness.mockRejectedValueOnce(new Error('来源就绪度暂时不可用')).mockResolvedValueOnce({ + totalVehicles: 1035, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, + kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: [] + }); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + + expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument(); + expect(screen.getByText('test-release')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /重试/ })); + + expect(await screen.findByText('1035 / 234')).toBeInTheDocument(); + expect(screen.queryByText('来源就绪度暂时不可用')).not.toBeInTheDocument(); + expect(mocks.sourceReadiness).toHaveBeenCalledTimes(2); +}); 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 50637180..df42d5ff 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx @@ -15,6 +15,7 @@ export default function OperationsPage() { return

运维质量

服务身份、主车辆档案和实时位置采用不同可核对口径,不用前端估算。

{health.isError ? : null} + {readiness.isError ? readiness.refetch()} /> : null}
运行版本{data?.runtime.platformRelease || '未注入'}{data?.runtime.dataMode || 'unknown'}
活跃连接{data?.activeConnections?.toLocaleString('zh-CN') ?? '—'}capacity-check