refactor(ui): converge semi history workspace
This commit is contained in:
@@ -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失败任务');
|
||||
|
||||
@@ -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}<i className="v2-history-summary-unit">ms</i></> : '—',
|
||||
note: `${visibleMetrics.length} / ${allMetrics.length} 个业务字段`,
|
||||
tone: 'neutral',
|
||||
emphasis: 'secondary'
|
||||
}
|
||||
];
|
||||
const historyFilterFields = <>
|
||||
@@ -585,31 +578,42 @@ export default function HistoryPage() {
|
||||
<WorkspacePanelHeader
|
||||
title="历史明细"
|
||||
description={keywords.length ? `${(result?.total ?? 0).toLocaleString('zh-CN')} 条记录 · ${resultSummary?.vehicleCount ?? 0} 辆车辆 · ${summarySources}` : '选择车辆和时间范围后查询'}
|
||||
meta={<span className="v2-history-result-meta"><Tag color="blue" type="light" size="small">{visibleMetrics.length} / {allMetrics.length} 字段</Tag>{resultSummary ? <span>{resultSummary.queryDurationMs} ms</span> : null}</span>}
|
||||
meta={<span className="v2-history-result-meta"><Tag color="blue" type="light" size="small">{visibleMetrics.length} / {allMetrics.length} 字段</Tag>{resultSummary ? <span className="v2-history-query-duration">查询 {resultSummary.queryDurationMs} ms</span> : null}</span>}
|
||||
actionsClassName={`v2-history-result-actions${mobileLayout ? ' is-mobile-actions' : ''}`}
|
||||
actions={mobileLayout ? keywords.length ? <>
|
||||
<Button className={`v2-history-table-trend v2-workspace-mobile-tool-button is-muted${trendExpanded ? ' is-expanded' : ''}`} theme="light" aria-label={trendExpanded ? '收起趋势' : '展开趋势'} aria-expanded={trendExpanded} icon={<IconChevronRight />} onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起' : '趋势'}</Button>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
visible={mobileToolsOpen}
|
||||
onVisibleChange={setMobileToolsOpen}
|
||||
visible={toolsOpen}
|
||||
onVisibleChange={setToolsOpen}
|
||||
contentClassName="v2-workspace-mobile-tools-menu"
|
||||
render={<Dropdown.Menu>
|
||||
<Dropdown.Item icon={<IconSetting />} onClick={() => { setMobileToolsOpen(false); setColumnSettingsOpen(true); }}>字段设置</Dropdown.Item>
|
||||
{exportAllowed ? <Dropdown.Item icon={<IconDownload />} onClick={() => { setMobileToolsOpen(false); setExportJobsOpen(true); }}>导出任务</Dropdown.Item> : null}
|
||||
<Dropdown.Item icon={<IconRefresh />} onClick={() => { setMobileToolsOpen(false); void dataQuery.refetch(); }}>刷新数据</Dropdown.Item>
|
||||
<Dropdown.Item icon={<IconSetting />} onClick={() => { setToolsOpen(false); setColumnSettingsOpen(true); }}>字段设置</Dropdown.Item>
|
||||
{exportAllowed ? <Dropdown.Item icon={<IconDownload />} onClick={() => { setToolsOpen(false); setExportJobsOpen(true); }}>导出任务</Dropdown.Item> : null}
|
||||
<Dropdown.Item icon={<IconRefresh />} onClick={() => { setToolsOpen(false); void dataQuery.refetch(); }}>刷新数据</Dropdown.Item>
|
||||
</Dropdown.Menu>}
|
||||
>
|
||||
<Button className="v2-workspace-mobile-tool-button is-muted" theme="light" aria-label="打开历史数据工具" aria-haspopup="menu" aria-expanded={mobileToolsOpen} icon={<IconSetting />}>工具</Button>
|
||||
<Button className="v2-workspace-mobile-tool-button is-muted" theme="light" aria-label="打开历史数据工具" aria-haspopup="menu" aria-expanded={toolsOpen} icon={<IconMore />}>工具</Button>
|
||||
</Dropdown>
|
||||
</> : null : <>
|
||||
<Button className={`v2-history-table-trend${trendExpanded ? ' is-expanded' : ''}`} theme="borderless" aria-label={trendExpanded ? '收起趋势' : '展开趋势'} aria-expanded={trendExpanded} icon={<IconChevronRight />} onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起趋势' : '聚合趋势'}</Button>
|
||||
<Button theme="borderless" aria-label="列设置" aria-haspopup="dialog" aria-expanded={columnSettingsOpen} aria-controls="v2-history-column-settings" icon={<IconSetting />} onClick={() => setColumnSettingsOpen((value) => !value)}>列设置</Button>
|
||||
<Button className={`v2-history-table-trend${trendExpanded ? ' is-expanded' : ''}`} theme="borderless" aria-label={trendExpanded ? '收起趋势' : '展开趋势'} aria-expanded={trendExpanded} icon={<IconChevronRight />} onClick={() => setTrendExpanded((value) => !value)}>{trendExpanded ? '收起' : '趋势'}</Button>
|
||||
<Button theme="borderless" aria-label="列设置" aria-haspopup="dialog" aria-expanded={columnSettingsOpen} aria-controls="v2-history-column-settings" icon={<IconSetting />} onClick={() => setColumnSettingsOpen((value) => !value)}>字段</Button>
|
||||
{exportAllowed ? <CreateExportButton disabled={!resultRows.length} request={exportRequest} /> : null}
|
||||
{exportAllowed ? <Button theme="borderless" aria-label="查看导出任务" aria-haspopup="dialog" aria-expanded={exportJobsOpen} aria-controls="v2-history-export-jobs" icon={<IconDownload />} onClick={() => setExportJobsOpen(true)}>导出任务</Button> : null}
|
||||
<Select aria-label="表格密度" value={density} onChange={(value) => setDensity(String(value) as typeof density)} optionList={[{ value: 'compact', label: '紧凑' }, { value: 'comfortable', label: '舒适' }]} />
|
||||
<Button theme="borderless" onClick={() => dataQuery.refetch()} aria-label="刷新历史数据" icon={<IconRefresh />} />
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
visible={toolsOpen}
|
||||
onVisibleChange={setToolsOpen}
|
||||
contentClassName="v2-history-tools-menu"
|
||||
render={<Dropdown.Menu>
|
||||
{exportAllowed ? <Dropdown.Item icon={<IconDownload />} onClick={() => { setToolsOpen(false); setExportJobsOpen(true); }}>导出任务</Dropdown.Item> : null}
|
||||
<Dropdown.Item icon={<IconSetting />} onClick={() => { setToolsOpen(false); setDensity((value) => value === 'compact' ? 'comfortable' : 'compact'); }}>{density === 'compact' ? '切换为舒适密度' : '切换为紧凑密度'}</Dropdown.Item>
|
||||
<Dropdown.Item icon={<IconRefresh />} onClick={() => { setToolsOpen(false); void dataQuery.refetch(); }}>刷新数据</Dropdown.Item>
|
||||
</Dropdown.Menu>}
|
||||
>
|
||||
<Button className="v2-history-more-button" theme="borderless" aria-label="打开历史数据更多操作" aria-haspopup="menu" aria-expanded={toolsOpen} icon={<IconMore />}>更多</Button>
|
||||
</Dropdown>
|
||||
</>}
|
||||
/>
|
||||
<ColumnVisibilityPanel visible={columnSettingsOpen} metrics={allMetrics} visibleKeys={visibleKeys} onToggle={toggleMetric} onShowAll={() => setVisibleMetrics(allMetrics.map((metric) => metric.key))} onReset={() => setVisibleMetrics(allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key))} onClose={() => setColumnSettingsOpen(false)} />
|
||||
|
||||
@@ -24258,3 +24258,63 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Semi UI history workspace convergence.
|
||||
* Keep high-frequency evidence actions visible and move low-frequency table
|
||||
* controls into one predictable menu so the table remains the primary surface.
|
||||
*/
|
||||
.v2-history-result-meta {
|
||||
gap: 6px;
|
||||
border: 1px solid #e0e8f2;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
padding: 3px 8px 3px 4px;
|
||||
}
|
||||
|
||||
.v2-history-result-meta > .semi-tag {
|
||||
min-height: 20px;
|
||||
margin: 0;
|
||||
border-radius: 999px;
|
||||
padding-inline: 7px;
|
||||
}
|
||||
|
||||
.v2-history-query-duration {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-history-summary-rail .v2-workspace-metric-list {
|
||||
grid-template-columns: 1.05fr .85fr 1.35fr;
|
||||
}
|
||||
|
||||
.v2-history-summary-rail .v2-workspace-metric-list > span:last-child > em {
|
||||
color: #718198;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.v2-history-result-actions > .semi-button,
|
||||
.v2-history-result-actions > .semi-dropdown > .semi-button {
|
||||
min-height: 32px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v2-history-result-actions > .v2-secondary-button {
|
||||
border-color: #d5e3f6;
|
||||
background: #f2f7ff;
|
||||
color: #155fc5;
|
||||
}
|
||||
|
||||
.v2-history-more-button[aria-expanded="true"] {
|
||||
background: #edf4fe;
|
||||
color: #155fc5;
|
||||
}
|
||||
|
||||
.v2-history-tools-menu {
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.v2-history-summary-rail .v2-workspace-metric-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user