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 8b9317ae..36acc507 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
@@ -54,6 +54,12 @@ function renderPage(initialEntry = '/history?keywords=OLDVIN&dateFrom=2026-07-16
return { ...view, client };
}
+async function openDesktopTool(name: RegExp | string) {
+ fireEvent.click(screen.getByRole('button', { name: '打开历史数据更多操作' }));
+ const item = await screen.findByRole('menuitem', { name });
+ fireEvent.click(item);
+}
+
test('renders protocol timestamps in Asia/Shanghai while preserving local values', () => {
expect(formatHistoryDateTime('2026-07-18T02:33:00Z')).toBe('2026-07-18 10:33:00');
expect(formatHistoryDateTime('2026-07-18T02:33:00+08:00')).toBe('2026-07-18 02:33:00');
@@ -141,12 +147,12 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
expect(view.container.querySelector('input[type="datetime-local"]')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-history-summary-rail')).toHaveClass('v2-workspace-metric-rail', 'is-queue');
expect(view.container.querySelector('.v2-history-summary-rail .v2-workspace-metric-list')).toHaveAttribute('aria-label', '历史数据查询统计信息');
- expect(view.container.querySelectorAll('.v2-history-summary-rail .v2-workspace-metric-list > span')).toHaveLength(4);
+ expect(view.container.querySelectorAll('.v2-history-summary-rail .v2-workspace-metric-list > span')).toHaveLength(3);
expect(view.container.querySelector('.v2-history-summary-rail .v2-workspace-metric-list > span.is-primary')).toHaveTextContent('结果行数1当前范围');
expect(view.container.querySelector('.v2-history-summary-rail .v2-workspace-metric-list')).toHaveTextContent('车辆数1辆1 个车辆标识');
expect(view.container.querySelector('.v2-history-summary-rail .v2-workspace-metric-list')).toHaveTextContent('数据来源1路JT808');
- expect(view.container.querySelector('.v2-history-summary-rail .v2-workspace-metric-list')).toHaveTextContent('查询耗时12ms1 / 1 个业务字段');
expect(screen.getByText('1 / 1 字段').closest('.semi-tag')).toBeInTheDocument();
+ expect(screen.getByText('查询 12 ms')).toHaveClass('v2-history-query-duration');
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');
@@ -331,7 +337,8 @@ test('releases export job state immediately after leaving the history page', asy
mocks.historyExports.mockResolvedValue([{ id: 'export-running', name: '运行中导出', status: 'running', progress: 30, format: 'csv', category: 'location', keywords: ['OLDVIN'], rowCount: 0, totalRows: 100, processedRows: 30, fileSizeBytes: 0, createdAt: '2026-07-16T04:00:00Z', updatedAt: '2026-07-16T04:00:01Z', evidence: 'test export' }]);
const { client, unmount } = renderPage();
- fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
+ await screen.findByText('旧车牌');
+ await openDesktopTool(/导出任务/);
expect(await screen.findByText('运行中导出')).toBeInTheDocument();
expect(client.getQueryCache().find({ queryKey: ['history-exports'] })).toBeDefined();
@@ -339,6 +346,25 @@ test('releases export job state immediately after leaving the history page', asy
await waitFor(() => expect(client.getQueryCache().find({ queryKey: ['history-exports'] })).toBeUndefined());
});
+test('keeps low-frequency desktop table controls in one working tools menu', async () => {
+ mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] });
+ mocks.historyData.mockResolvedValue(historyData('OLDVIN', '旧车牌', 'old-as-of'));
+ mocks.historySeries.mockResolvedValue(historySeries('OLDVIN', '旧车牌', 'old-as-of'));
+ mocks.historyExports.mockResolvedValue([]);
+ const view = renderPage();
+
+ await screen.findByText('旧车牌');
+ expect(view.container.querySelector('.v2-history-table-card')).toHaveClass('is-compact');
+ await openDesktopTool(/切换为舒适密度/);
+ expect(view.container.querySelector('.v2-history-table-card')).toHaveClass('is-comfortable');
+ await openDesktopTool(/切换为紧凑密度/);
+ expect(view.container.querySelector('.v2-history-table-card')).toHaveClass('is-compact');
+
+ const historyRequests = mocks.historyData.mock.calls.length;
+ await openDesktopTool(/刷新数据/);
+ await waitFor(() => expect(mocks.historyData.mock.calls.length).toBeGreaterThan(historyRequests));
+});
+
test('opens a working column visibility panel and preserves non-hideable identity columns', async () => {
const data = historyData('OLDVIN', '旧车牌', 'old-as-of');
data.columns = [metric, optionalMetric];
@@ -409,7 +435,8 @@ test('shows only the authenticated customer export workspace with owner and scop
}]);
renderPage();
- fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
+ await screen.findByText('旧车牌');
+ await openDesktopTool(/导出任务/);
expect(await screen.findByText(/customer-a · 1 辆/)).toBeInTheDocument();
expect(screen.getByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
expect(screen.getByRole('list', { name: '历史数据导出任务摘要' })).toHaveTextContent('生成中0排队与执行可下载1已完成任务需处理0失败任务');
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 3fc89997..1c12eddd 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 { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
+import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconMore, 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';
@@ -370,7 +370,7 @@ export default function HistoryPage() {
const [trendExpanded, setTrendExpanded] = useState(false);
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
const [exportJobsOpen, setExportJobsOpen] = useState(false);
- const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
+ const [toolsOpen, setToolsOpen] = useState(false);
useEffect(() => {
if (!mobileLayout) return;
setLimit(MOBILE_HISTORY_PAGE_SIZE);
@@ -487,13 +487,6 @@ export default function HistoryPage() {
note: summarySources,
tone: 'primary',
emphasis: 'secondary'
- },
- {
- label: '查询耗时',
- value: summaryLoading ? '—' : resultSummary ? <>{resultSummary.queryDurationMs}ms> : '—',
- note: `${visibleMetrics.length} / ${allMetrics.length} 个业务字段`,
- tone: 'neutral',
- emphasis: 'secondary'
}
];
const historyFilterFields = <>
@@ -585,31 +578,42 @@ export default function HistoryPage() {
{visibleMetrics.length} / {allMetrics.length} 字段{resultSummary ? {resultSummary.queryDurationMs} ms : null}}
+ meta={{visibleMetrics.length} / {allMetrics.length} 字段{resultSummary ? 查询 {resultSummary.queryDurationMs} ms : null}}
actionsClassName={`v2-history-result-actions${mobileLayout ? ' is-mobile-actions' : ''}`}
actions={mobileLayout ? keywords.length ? <>
} onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起' : '趋势'}
- } onClick={() => { setMobileToolsOpen(false); setColumnSettingsOpen(true); }}>字段设置
- {exportAllowed ? } onClick={() => { setMobileToolsOpen(false); setExportJobsOpen(true); }}>导出任务 : null}
- } onClick={() => { setMobileToolsOpen(false); void dataQuery.refetch(); }}>刷新数据
+ } onClick={() => { setToolsOpen(false); setColumnSettingsOpen(true); }}>字段设置
+ {exportAllowed ? } onClick={() => { setToolsOpen(false); setExportJobsOpen(true); }}>导出任务 : null}
+ } onClick={() => { setToolsOpen(false); void dataQuery.refetch(); }}>刷新数据
}
>
- }>工具
+ }>工具
> : null : <>
- } onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起趋势' : '聚合趋势'}
- } onClick={() => setColumnSettingsOpen((value) => !value)}>列设置
+ } onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起' : '趋势'}
+ } onClick={() => setColumnSettingsOpen((value) => !value)}>字段
{exportAllowed ? : null}
- {exportAllowed ? } onClick={() => setExportJobsOpen(true)}>导出任务 : null}
-