unify Semi UI query metric rails

This commit is contained in:
lingniu
2026-07-18 09:02:54 +08:00
parent fb7cb4c6d6
commit 1688ba1b50
7 changed files with 345 additions and 342 deletions

View File

@@ -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'));
});

View File

@@ -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 <div className="v2-history-page">
<MonitorReturnBar />
@@ -348,31 +375,15 @@ export default function HistoryPage() {
<div className="v2-history-toolbar-actions"><Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={!parseHistoryKeywords(draft.keywords).length}></Button><Button className="v2-secondary-button" theme="light" onClick={reset}></Button>{exportAllowed ? <CreateExportButton disabled={!resultRows.length} request={{ keywords, category: criteria.category, protocol: criteria.protocol || undefined, dateFrom: criteria.dateFrom, dateTo: criteria.dateTo, metrics: visibleKeys, format: 'csv' }} /> : <Tag className="v2-history-permission-tag" color="grey" type="light" size="large"></Tag>}</div>
</form>
</WorkspaceFilterPanel>
<Card className="v2-history-context-card" bodyStyle={{ padding: 0 }}>
<div className="v2-history-context">
<div className="v2-history-metrics">
<div className="v2-history-metrics-heading">
<strong></strong>
<small>{visibleMetrics.length} / {allMetrics.length}</small>
</div>
<div
className="v2-history-metric-scroll"
tabIndex={mobileLayout ? 0 : undefined}
aria-label={mobileLayout ? '当前显示字段,可左右滑动' : '当前显示字段'}
>
{visibleMetrics.map((metric) => <Tag className="v2-history-metric-tag" color="blue" type="light" size="large" closable tabIndex={0} aria-label={`已选字段 ${metric.label}${metric.unit ? ` ${metric.unit}` : ''},点击取消显示`} onClick={() => 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})` : ''}</Tag>)}
{allMetrics.length > visibleMetrics.length ? <span> {allMetrics.length - visibleMetrics.length} </span> : !allMetrics.length ? <span></span> : null}
</div>
<Button className="v2-history-metrics-manage" theme="borderless" icon={<IconSetting />} aria-label="管理字段" onClick={() => setColumnSettingsOpen(true)} disabled={!allMetrics.length}></Button>
</div>
<div className="v2-history-summary" aria-label="当前查询摘要">
<div className="is-primary"><small></small><strong>{result?.total?.toLocaleString('zh-CN') ?? '—'}</strong></div>
<div><small></small><strong>{resultSummary?.vehicleCount ?? '—'}</strong></div>
<div><small></small><strong className="is-source" title={summarySources}>{summarySources}</strong></div>
<div><small></small><strong>{resultSummary ? `${resultSummary.queryDurationMs} ms` : '—'}</strong></div>
</div>
</div>
</Card>
{keywords.length ? <WorkspaceMetricRail
className="v2-history-summary-rail"
cardClassName="v2-history-summary-card"
secondaryClassName="v2-history-summary-secondary"
primaryValueClassName="v2-history-summary-value"
ariaLabel="历史数据查询统计信息"
primary={summaryPrimary}
secondary={summarySecondary}
/> : null}
{dataQuery.isError ? <InlineError message={dataQuery.error instanceof Error ? dataQuery.error.message : '历史查询失败'} onRetry={() => dataQuery.refetch()} /> : null}
<div className={`v2-history-workspace${selectedRow && !mobileLayout ? ' is-inspector-open' : ''}`}>
<div className={`v2-history-main${trendExpanded ? ' has-expanded-trend' : ''}`}>
@@ -381,7 +392,7 @@ export default function HistoryPage() {
<WorkspacePanelHeader
title="历史明细"
description={keywords.length ? `${(result?.total ?? 0).toLocaleString('zh-CN')} 条记录 · ${resultSummary?.vehicleCount ?? 0} 辆车辆 · ${summarySources}` : '选择车辆和时间范围后查询'}
meta={resultSummary ? `${resultSummary.queryDurationMs} ms` : undefined}
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>}
actionsClassName={`v2-history-result-actions${mobileLayout ? ' is-mobile-actions' : ''}`}
actions={<>
<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>

View File

@@ -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 <Card className="v2-mileage-summary" aria-label="里程查询统计信息" bodyStyle={{ padding: 0 }}>
<Card className="v2-mileage-summary-card is-primary" bodyStyle={{ padding: 0 }} aria-label={`${primary[0]}${primary[1]}${primary[2]}`}><small>{primary[0]}</small><strong className="v2-mileage-summary-value">{primary[1]}</strong><span title={primary[2]}>{primary[2]}</span></Card>
<CardGroup className="v2-mileage-summary-secondary" type="grid" spacing={0}>{secondary.map(([label, value, note], index) => <Card className={`v2-mileage-summary-card is-${['fleet', 'range', 'average'][index]}`} bodyStyle={{ padding: 0 }} aria-label={`${label}${value}${note}`} key={label}><small>{label}</small><strong>{value}</strong><span title={note}>{note}</span></Card>)}</CardGroup>
</Card>;
return <WorkspaceMetricRail
className="v2-mileage-summary"
cardClassName="v2-mileage-summary-card"
secondaryClassName="v2-mileage-summary-secondary"
primaryValueClassName="v2-mileage-summary-value"
ariaLabel="里程查询统计信息"
primary={{ label: primary[0], value: primary[1], note: primary[2] }}
secondary={secondary.map(([label, value, note], index) => ({ label, value, note, className: `is-${['fleet', 'range', 'average'][index]}` }))}
/>;
}
type VehicleMileageMatrix = VehicleOption & { days: Map<string, number>; sources: Map<string, string>; totalMileageKm: number };