diff --git a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx
index 56d90b4f..18d60b78 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.test.tsx
@@ -117,6 +117,21 @@ test('supports searching and selecting license plates before querying exact VINs
expect(screen.getByTitle('LTEST000000000001')).toHaveTextContent('粤A12345');
});
+test('shows and recovers from a failed license plate candidate query', async () => {
+ prepareData();
+ mocks.vehicles.mockRejectedValueOnce(new Error('车辆目录查询超时')).mockResolvedValueOnce({ items: [{ vin: 'LTEST000000000001', plate: '粤A12345' }], total: 1, limit: 12, offset: 0 });
+ renderPage();
+
+ fireEvent.focus(screen.getByRole('textbox', { name: '搜索车牌' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('车辆目录查询超时');
+ expect(screen.queryByText('没有匹配的车牌')).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: '重试' }));
+
+ expect(await screen.findByRole('option', { name: /粤A12345/ })).toBeInTheDocument();
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ expect(mocks.vehicles).toHaveBeenCalledTimes(2);
+});
+
test('lets users disable mileage sources and persists the source priority', async () => {
prepareData();
renderPage('/statistics?vins=LTEST000000000001&dateFrom=2026-07-13&dateTo=2026-07-14');
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx
index e6eb91da..012be140 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx
@@ -174,10 +174,11 @@ function VehicleMultiSelect({ value, onChange }: { value: VehicleOption[]; onCha
{open ?
车牌候选{value.length}/{MAX_SELECTED_VEHICLES} 已选
{candidates.isLoading ?
正在搜索车辆…
: null}
+ {!candidates.isLoading && candidates.isError ?
{candidates.error instanceof Error ? candidates.error.message : '车牌候选加载失败'}
: null}
{!candidates.isLoading && options.map((vehicle) =>
)}
- {!candidates.isLoading && !options.length ?
没有匹配的车牌
: null}
+ {!candidates.isLoading && !candidates.isError && !options.length ?
没有匹配的车牌
: null}
: null}
支持车牌关键字搜索,最多选择 {MAX_SELECTED_VEHICLES} 辆
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx
index 9fe7a2f8..8dd26b84 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx
@@ -66,6 +66,21 @@ test('keeps committed track criteria authoritative to same-route navigation and
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '0');
});
+test('distinguishes a failed vehicle lookup from an empty result and retries in place', async () => {
+ mocks.vehicles.mockRejectedValueOnce(new Error('车辆目录暂时不可用')).mockResolvedValueOnce({ items: [{ vin: 'LTEST000000000001', plate: '粤A12345' }], total: 1, limit: 10, offset: 0 });
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ render();
+
+ fireEvent.focus(screen.getByRole('textbox', { name: '搜索轨迹车辆' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('车辆目录暂时不可用');
+ expect(screen.queryByText('没有匹配车辆')).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: '重试' }));
+
+ expect(await screen.findByRole('option', { name: /粤A12345/ })).toBeInTheDocument();
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ expect(mocks.vehicles).toHaveBeenCalledTimes(2);
+});
+
test('renders a map-first replay workspace and connects stop, event, and panel interactions', async () => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render();
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx
index 84966778..7acaaa90 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx
@@ -113,11 +113,12 @@ function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange:
{open ?
{candidates.isFetching ?
正在搜索车辆
: null}
+ {!candidates.isFetching && candidates.isError ?
{candidates.error instanceof Error ? candidates.error.message : '车辆候选加载失败'}
: null}
{!candidates.isFetching && (candidates.data?.items ?? []).map((vehicle) =>
)}
- {!candidates.isFetching && !(candidates.data?.items.length) ?
没有匹配车辆
: null}
+ {!candidates.isFetching && !candidates.isError && !(candidates.data?.items.length) ?
没有匹配车辆
: null}
: null}
;
}
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css
index d3096295..7b84a99c 100644
--- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css
+++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css
@@ -1008,6 +1008,9 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-mileage-options > button span { overflow: hidden; color: #7d899a; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; text-overflow: ellipsis; }
.v2-mileage-options > button em { color: var(--v2-blue); font-size: 9px; font-style: normal; }
.v2-mileage-options > p { margin: 0; padding: 16px; color: var(--v2-muted); text-align: center; font-size: 10px; }
+.v2-vehicle-option-error { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 10px; background: #fff8f8; padding: 9px 11px; color: #b44848; font-size: 10px; line-height: 1.45; }
+.v2-vehicle-option-error > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.v2-vehicle-option-error > button { height: 28px; flex: 0 0 auto; border: 1px solid #efc3c3; border-radius: 6px; background: #fff; padding: 0 10px; color: #ad4141; cursor: pointer; font-size: 10px; font-weight: 700; }
.v2-mileage-source-strategy { position: relative; align-self: start; margin-top: 18px; }
.v2-mileage-source-trigger { display: flex; height: 40px; align-items: center; gap: 6px; border: 1px solid #d7e0ea; border-radius: 7px; background: #fff; padding: 0 11px; color: #56657a; cursor: pointer; font-size: 11px; font-weight: 600; white-space: nowrap; transition: border-color .16s ease, background .16s ease; }
.v2-mileage-source-trigger:hover, .v2-mileage-source-trigger[aria-expanded="true"] { border-color: #a8bad2; background: #f8fafc; }