diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx
index 5778acc3..4d980bb6 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx
@@ -205,7 +205,7 @@ test('uses one focused Semi bottom SideSheet for the complete mobile history que
mocks.historyExports.mockResolvedValue([]);
renderPage('/history');
- await screen.findByText('选择车辆开始查询');
+ await screen.findByText('选择车辆,开始核验历史证据');
const trigger = screen.getByRole('button', { name: '修改查询范围:请选择车辆' });
expect(trigger).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('button', { name: '展开趋势' })).not.toBeInTheDocument();
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
index 450193fa..ffa8711b 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
-import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
+import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
@@ -8,7 +8,7 @@ import type { HistoryDataResponse, HistoryDataRow, HistoryExportJob, HistoryExpo
import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history';
import { downloadBlob } from '../domain/download';
import { formatShanghaiDateTime } from '../domain/formatters';
-import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
+import { InlineError, PanelEmpty, PanelLoading, WorkspaceEmptyGuide } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
@@ -593,11 +593,26 @@ export default function HistoryPage() {
{dataQuery.isPending && keywords.length
?
: !resultRows.length
- ? } aria-label={keywords.length ? '调整查询范围' : '选择车辆并查询'} onClick={() => setFiltersCollapsed(false)}>{keywords.length ? '调整查询范围' : '选择车辆并查询'} : null}
- />
+ ? keywords.length
+ ? } aria-label="调整查询范围" onClick={() => setFiltersCollapsed(false)}>调整查询范围}
+ />
+ : }
+ title="选择车辆,开始核验历史证据"
+ description="一次最多查询 5 台车辆,结果会保留协议来源、质量说明与 RAW 证据。"
+ steps={[
+ { icon: , title: '选择车辆', description: '支持车牌、VIN 与批量粘贴' },
+ { icon: , title: '确认时间', description: '按上海时区精确到分钟' },
+ { icon: , title: '核验数据', description: '位置、RAW 与日里程统一检索' }
+ ]}
+ action={} aria-label="选择车辆并查询" onClick={() => setFiltersCollapsed(false)}>选择车辆并查询}
+ />
: null}
{keywords.length ? : null}
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 b3ee6ba0..0ce7d5f2 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx
@@ -11,7 +11,7 @@ import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../.
import { protocolDisplayLabel } from '../domain/protocols';
import { buildTrackRailSelection, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, trackPointMileageAvailable, type TrackAddressCoordinate } from '../domain/track';
import { TrackMap } from '../map/TrackMap';
-import { InlineError } from '../shared/AsyncState';
+import { InlineError, WorkspaceEmptyGuide } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { ProtocolTag } from '../shared/ProtocolTag';
import { SegmentedTabs } from '../shared/SegmentedTabs';
@@ -513,7 +513,18 @@ export default function TrackPage() {
{playing ? '播放中,暂停地址解析' : addressSettling ? '位置已变化,等待地址解析…' : addressQuery.isFetching ? '地址解析中…' : addressQuery.data?.formattedAddress || `${current?.longitude.toFixed(6)}, ${current?.latitude.toFixed(6)}`}
- > : } title="先选择车辆,再开始轨迹回放" description="地图会显示完整路径、停留点、事件点和播放位置。">} onClick={() => setRailCollapsed(false)}>选择车辆}
+ > : }
+ title="先选择车辆,再开始轨迹回放"
+ description="地图会呈现完整路径、停留点、事件点与逐帧播放位置。"
+ steps={[
+ { icon: , title: '选择车辆', description: '车牌或 VIN 精确定位' },
+ { icon: , title: '确认来源', description: '自动选择最佳定位来源' },
+ { icon: , title: '播放核验', description: '逐点查看速度、里程与事件' }
+ ]}
+ action={} onClick={() => setRailCollapsed(false)}>选择车辆}
+ />}
{query.isFetching ? 正在读取并加工历史轨迹
: null}
{query.isError ? query.refetch()} />
: null}
diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
index 30e084c2..f925d785 100644
--- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
+++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
@@ -183,7 +183,8 @@ describe('V2 production entry', () => {
expect(corePageSources.HistoryPage).toContain('');
expect(corePageSources.HistoryPage).not.toContain('job) => {
expect(corePageSources.TrackPage).toContain('
} title="先选择车辆,再开始轨迹回放"');
+ expect(corePageSources.TrackPage).toContain(' {
expect(action).toHaveBeenCalledTimes(1);
});
+test('renders a guided Semi empty workspace with clear ordered actions', () => {
+ const action = vi.fn();
+ const view = render(开始查询}
+ />);
+
+ expect(view.container.querySelector('.v2-workspace-empty-guide')).toBeInTheDocument();
+ expect(view.container.querySelectorAll('.v2-workspace-empty-guide-step.semi-card')).toHaveLength(3);
+ expect(screen.getByText('历史证据')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: '开始查询' }));
+ expect(action).toHaveBeenCalledTimes(1);
+});
+
test('announces recoverable errors and exposes a Semi retry action', () => {
const retry = vi.fn();
render();
diff --git a/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx b/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx
index e8a64d13..4bc7f3fe 100644
--- a/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx
@@ -1,5 +1,5 @@
import { IconAlertTriangle, IconRefresh } from '@douyinfe/semi-icons';
-import { Button, Empty, Spin, Typography } from '@douyinfe/semi-ui';
+import { Button, Card, CardGroup, Empty, Spin, Typography } from '@douyinfe/semi-ui';
import type { ReactNode } from 'react';
type StateScope = 'page' | 'panel' | 'inline';
@@ -75,6 +75,51 @@ export function PanelEmpty({
return ;
}
+export type WorkspaceEmptyGuideStep = {
+ title: string;
+ description: string;
+ icon?: ReactNode;
+};
+
+export function WorkspaceEmptyGuide({
+ title,
+ description,
+ steps,
+ icon,
+ eyebrow,
+ action,
+ secondaryAction,
+ className = ''
+}: {
+ title: string;
+ description: string;
+ steps: WorkspaceEmptyGuideStep[];
+ icon?: ReactNode;
+ eyebrow?: string;
+ action?: ReactNode;
+ secondaryAction?: ReactNode;
+ className?: string;
+}) {
+ const classes = ['v2-state-surface', 'is-empty', 'is-panel', 'v2-workspace-empty-guide', className].filter(Boolean).join(' ');
+
+ return
+ {eyebrow ? {eyebrow} : null}
+ {icon} : undefined}
+ title={title}
+ description={description}
+ />
+
+ {steps.map((step, index) =>
+ {step.icon ?? index + 1}
+ {step.title}{step.description}
+ )}
+
+ {action || secondaryAction ? {action}{secondaryAction}
: null}
+ ;
+}
+
export function InlineError({ message, onRetry }: { message: string; onRetry?: () => void }) {
return .semi-card-body {
+ display: grid;
+ min-height: 72px;
+ grid-template-columns: 32px minmax(0, 1fr);
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px !important;
+ text-align: left;
+}
+
+.v2-workspace-empty-guide-step-index {
+ display: grid;
+ width: 30px;
+ height: 30px;
+ place-items: center;
+ border-radius: 9px;
+ background: #f0f5fc;
+ color: #3976cc;
+ font-size: 13px;
+ font-weight: 800;
+}
+
+.v2-workspace-empty-guide-step-index .semi-icon {
+ font-size: 15px;
+}
+
+.v2-workspace-empty-guide-step > .semi-card-body > span:last-child {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+}
+
+.v2-workspace-empty-guide-step .semi-typography {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.v2-workspace-empty-guide-step .semi-typography:first-child {
+ color: #33485f;
+ font-size: 12px;
+}
+
+.v2-workspace-empty-guide-step .semi-typography:last-child {
+ color: #8290a2;
+ font-size: 9px;
+ line-height: 1.45;
+}
+
+.v2-workspace-empty-guide-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ margin-top: 18px;
+}
+
+.v2-workspace-empty-guide-actions > .semi-button {
+ min-height: 38px;
+ border-radius: 8px;
+ padding-inline: 18px;
+ font-weight: 700;
+}
+
+.v2-history-empty.v2-workspace-empty-guide {
+ position: sticky;
+ left: 0;
+ min-height: 100%;
+}
+
+.v2-track-empty-state.semi-card {
+ width: min(680px, calc(100% - 44px));
+}
+
+.v2-track-empty-state > .semi-card-body > .v2-workspace-empty-guide {
+ width: 100%;
+ padding: 22px;
+}
+
+.v2-track-empty-state .v2-workspace-empty-guide-message .semi-empty-description {
+ max-width: 480px;
+}
+
+.v2-track-empty-state .v2-workspace-empty-guide strong {
+ margin-top: 0;
+}
+
+@media (max-width: 680px) {
+ .v2-workspace-empty-guide {
+ width: calc(100% - 20px);
+ padding: 20px 10px;
+ }
+
+ .v2-workspace-empty-guide-eyebrow.semi-typography {
+ margin-bottom: 7px;
+ font-size: 9px;
+ }
+
+ .v2-workspace-empty-guide-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 12px;
+ }
+
+ .v2-workspace-empty-guide-message .semi-empty-title {
+ font-size: 15px;
+ }
+
+ .v2-workspace-empty-guide-message .semi-empty-description {
+ max-width: 310px;
+ font-size: 10px;
+ }
+
+ .v2-workspace-empty-guide-steps.semi-card-group {
+ gap: 6px;
+ margin-top: 16px;
+ }
+
+ .v2-workspace-empty-guide-step > .semi-card-body {
+ min-height: 92px;
+ grid-template-columns: 1fr;
+ align-content: start;
+ justify-items: center;
+ gap: 7px;
+ padding: 9px 6px !important;
+ text-align: center;
+ }
+
+ .v2-workspace-empty-guide-step-index {
+ width: 28px;
+ height: 28px;
+ }
+
+ .v2-workspace-empty-guide-step .semi-typography:first-child {
+ font-size: 10px;
+ }
+
+ .v2-workspace-empty-guide-step .semi-typography:last-child {
+ display: -webkit-box;
+ font-size: 8px;
+ line-height: 1.35;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ }
+
+ .v2-workspace-empty-guide-actions {
+ width: 100%;
+ margin-top: 15px;
+ }
+
+ .v2-workspace-empty-guide-actions > .semi-button {
+ min-width: 168px;
+ min-height: 42px;
+ }
+
+ .v2-history-empty.v2-workspace-empty-guide {
+ min-height: 430px;
+ }
+
+ .v2-track-empty-state.semi-card {
+ width: calc(100% - 24px);
+ transform: translate(-50%, calc(-50% - 44px));
+ }
+
+ .v2-track-empty-state > .semi-card-body > .v2-workspace-empty-guide {
+ padding: 17px 9px;
+ }
+
+ .v2-track-empty-state .v2-workspace-empty-guide-message .semi-empty-description {
+ max-width: 300px;
+ }
+}