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 5eb504c9..2021eebb 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
@@ -110,11 +110,14 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
const view = renderPage();
expect((await screen.findAllByText('旧车牌')).length).toBeGreaterThan(0);
expect(screen.queryByText('OLDVIN-evidence')).not.toBeInTheDocument();
- for (const className of ['v2-history-filter-card', 'v2-history-context-card', 'v2-history-table-card']) {
+ for (const className of ['v2-history-filter-card', 'v2-history-summary-rail', 'v2-history-table-card']) {
expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
}
- expect(view.container.querySelector('.v2-history-context .v2-history-metrics')).toBeInTheDocument();
- expect(view.container.querySelector('.v2-history-context .v2-history-summary')).toBeInTheDocument();
+ expect(view.container.querySelector('.v2-history-summary-rail')).toHaveAttribute('aria-label', '历史数据查询统计信息');
+ expect(view.container.querySelector('.v2-history-summary-secondary.semi-card-group')).toBeInTheDocument();
+ expect(view.container.querySelectorAll('.v2-history-summary-card.semi-card')).toHaveLength(4);
+ expect(view.container.querySelector('.v2-history-summary-card.is-primary .v2-history-summary-value')).toHaveTextContent('1');
+ expect(screen.getByText('1 / 1 字段').closest('.semi-tag')).toBeInTheDocument();
expect(view.container.querySelector('.v2-history-result-actions.v2-workspace-panel-actions')).toBeInTheDocument();
expect(view.container.querySelector('.v2-history-evidence')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-history-workspace')).not.toHaveClass('is-inspector-open');
@@ -180,8 +183,8 @@ test('renders only selectable Semi history cards on mobile', async () => {
expect(action).toHaveAttribute('aria-expanded', 'false');
expect(view.container.querySelector('.v2-history-table-scroll.is-mobile-scroll')).toHaveAttribute('aria-label', '历史明细列表,可上下滚动');
expect(view.container.querySelector('.v2-history-table-scroll.is-mobile-scroll')).toHaveAttribute('tabindex', '0');
- expect(view.container.querySelector('.v2-history-metric-scroll')).toHaveAttribute('aria-label', '当前显示字段,可左右滑动');
- expect(view.container.querySelector('.v2-history-metric-scroll')).toHaveAttribute('tabindex', '0');
+ expect(view.container.querySelector('.v2-history-metric-scroll')).not.toBeInTheDocument();
+ expect(view.container.querySelector('.v2-history-summary-rail')).toBeInTheDocument();
expect(view.container.querySelector('.v2-history-result-actions')).toHaveClass('is-mobile-actions');
expect(screen.queryByText('MOBILEVIN-evidence')).not.toBeInTheDocument();
expect((mocks.historyData.mock.calls[0]?.[0] as URLSearchParams).get('limit')).toBe('20');
@@ -238,13 +241,8 @@ test('opens a working column visibility panel and preserves non-hideable identit
expect(await screen.findByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument();
expect(screen.queryByRole('columnheader', { name: 'SOC (%)' })).not.toBeInTheDocument();
- const metricScroll = view.container.querySelector('.v2-history-metric-scroll');
- const manageMetrics = screen.getByRole('button', { name: '管理字段' });
- expect(metricScroll).toBeInTheDocument();
- expect(metricScroll).toHaveAttribute('aria-label', '当前显示字段');
- expect(metricScroll?.contains(screen.getByLabelText(/已选字段 速度/))).toBe(true);
- expect(metricScroll?.contains(manageMetrics)).toBe(false);
- expect(manageMetrics.parentElement).toHaveClass('v2-history-metrics');
+ expect(view.container.querySelector('.v2-history-metric-scroll')).not.toBeInTheDocument();
+ expect(screen.getByText('1 / 2 字段').closest('.semi-tag')).toBeInTheDocument();
const columnSettingsButton = screen.getByRole('button', { name: '列设置' });
fireEvent.click(columnSettingsButton);
@@ -326,8 +324,7 @@ test('uses selected chartable fields for trends and omits empty RAW evidence UI'
expect((mocks.historySeries.mock.calls[mocks.historySeries.mock.calls.length - 1]?.[0] as URLSearchParams).get('metrics')).toBe('speedKmh,totalMileageKm');
expect(screen.queryByText('RAW 证据')).not.toBeInTheDocument();
- const mileageToggle = screen.getAllByLabelText(/已选字段 .*点击取消显示/).find((button) => button.textContent?.includes('总里程'));
- expect(mileageToggle).toBeDefined();
- fireEvent.click(mileageToggle!);
+ fireEvent.click(screen.getByRole('button', { name: '列设置' }));
+ fireEvent.click(screen.getByRole('checkbox', { name: '总里程 (km)' }));
await waitFor(() => expect((mocks.historySeries.mock.calls[mocks.historySeries.mock.calls.length - 1]?.[0] as URLSearchParams).get('metrics')).toBe('speedKmh'));
});
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 3c6dd673..6055d03b 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
@@ -11,6 +11,7 @@ import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { TablePagination } from '../shared/TablePagination';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
+import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { detailTriggerRow } from '../shared/detailTriggerRow';
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
@@ -327,6 +328,32 @@ export default function HistoryPage() {
const totalPages = Math.max(1, Math.ceil((result?.total ?? 0) / limit));
const page = Math.floor(offset / limit) + 1;
const summarySources = resultSummary?.sources?.join('、') || '—';
+ const summaryLoading = keywords.length > 0 && dataQuery.isFetching && !result;
+ const summaryPrimary = {
+ label: '结果行数',
+ value: summaryLoading ? '—' : (result?.total ?? 0).toLocaleString('zh-CN'),
+ note: summaryLoading ? '正在读取当前筛选范围' : `${resultSummary?.vehicleCount ?? 0} 辆车辆`
+ };
+ const summarySecondary = [
+ {
+ label: '车辆数',
+ value: summaryLoading ? '—' : String(resultSummary?.vehicleCount ?? 0),
+ note: `当前筛选 ${keywords.length} 个车辆标识`,
+ className: 'is-vehicles'
+ },
+ {
+ label: '数据来源',
+ value: summaryLoading ? '—' : `${resultSummary?.sources?.length ?? 0} 个来源`,
+ note: summarySources,
+ className: 'is-sources'
+ },
+ {
+ label: '查询耗时',
+ value: summaryLoading ? '—' : resultSummary ? `${resultSummary.queryDurationMs} ms` : '—',
+ note: `${visibleMetrics.length} / ${allMetrics.length} 个业务字段`,
+ className: 'is-duration'
+ }
+ ];
return
@@ -348,31 +375,15 @@ export default function HistoryPage() {
{exportAllowed ? : 只读}
-
-
-
-
- 显示字段
- {visibleMetrics.length} / {allMetrics.length}
-
-
- {visibleMetrics.map((metric) => toggleMetric(metric.key)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleMetric(metric.key); } }} onClose={(_, event) => { event.stopPropagation(); toggleMetric(metric.key); }} key={metric.key}>{metric.label}{metric.unit ? ` (${metric.unit})` : ''})}
- {allMetrics.length > visibleMetrics.length ? 另有 {allMetrics.length - visibleMetrics.length} 项可选 : !allMetrics.length ? 查询后加载可用指标 : null}
-
-
} aria-label="管理字段" onClick={() => setColumnSettingsOpen(true)} disabled={!allMetrics.length}>管理字段
-
-
-
结果行数{result?.total?.toLocaleString('zh-CN') ?? '—'}
-
车辆数{resultSummary?.vehicleCount ?? '—'}
-
数据源{summarySources}
-
查询耗时{resultSummary ? `${resultSummary.queryDurationMs} ms` : '—'}
-
-
-
+ {keywords.length ?
: null}
{dataQuery.isError ?
dataQuery.refetch()} /> : null}
@@ -381,7 +392,7 @@ export default function HistoryPage() {
{visibleMetrics.length} / {allMetrics.length} 字段{resultSummary ? {resultSummary.queryDurationMs} ms : null}}
actionsClassName={`v2-history-result-actions${mobileLayout ? ' is-mobile-actions' : ''}`}
actions={<>
} onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起趋势' : '聚合趋势'}
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 c2c5d59f..d97ea40f 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx
@@ -1,5 +1,5 @@
import { IconArrowDown, IconArrowUp, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
-import { Button, Card, CardGroup, Empty, Input, SideSheet, Spin, Switch, Table, Tag } from '@douyinfe/semi-ui';
+import { Button, Card, Empty, Input, SideSheet, Spin, Switch, Table, Tag } from '@douyinfe/semi-ui';
import { useQuery } from '@tanstack/react-query';
import { type CSSProperties, FormEvent, memo, type RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
@@ -13,6 +13,7 @@ import { TablePagination } from '../shared/TablePagination';
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
+import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
import { monitorReturnFromParams, preserveMonitorReturn } from '../routing/monitorContext';
@@ -249,10 +250,15 @@ function SummaryRail({ data, criteria, fleetTotal, loading }: { data?: MileageSt
];
const primary = items[2];
const secondary = [items[0], items[1], items[3]];
- return
- {primary[0]}{primary[1]}{primary[2]}
- {secondary.map(([label, value, note], index) => {label}{value}{note})}
- ;
+ return ({ label, value, note, className: `is-${['fleet', 'range', 'average'][index]}` }))}
+ />;
}
type VehicleMileageMatrix = VehicleOption & { days: Map; sources: Map; totalMileageKm: number };
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 e2cead77..3cd984e9 100644
--- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
+++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
@@ -17,6 +17,7 @@ const profileSyncPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/pages
const sourceEvidenceSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/VehicleSourceEvidencePanel.tsx'), 'utf8');
const mobileFilterToggleSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/MobileFilterToggle.tsx'), 'utf8');
const workspaceFilterPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceFilterPanel.tsx'), 'utf8');
+const workspaceMetricRailSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceMetricRail.tsx'), 'utf8');
const segmentedTabsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/SegmentedTabs.tsx'), 'utf8');
const metricActionSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/MetricActionButton.tsx'), 'utf8');
const tablePaginationSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/TablePagination.tsx'), 'utf8');
@@ -127,10 +128,13 @@ describe('V2 production entry', () => {
expect(corePageSources.HistoryPage).toContain('v2-history-column-sidesheet');
expect(corePageSources.HistoryPage).toContain('');
+ expect(corePageSources.HistoryPage).toContain(' {
expect(corePageSources.TrackPage).not.toContain('