feat: unify mileage metrics with semi ui

This commit is contained in:
lingniu
2026-07-19 12:02:10 +08:00
parent 2e87378c92
commit 031e22de79
6 changed files with 124 additions and 29 deletions

View File

@@ -60,15 +60,18 @@ test('renders only the desktop matrix with dates as columns and a period total',
expect(screen.queryByText('车辆里程排名')).not.toBeInTheDocument(); expect(screen.queryByText('车辆里程排名')).not.toBeInTheDocument();
expect(screen.getByText('已绑定主车辆')).toBeInTheDocument(); expect(screen.getByText('已绑定主车辆')).toBeInTheDocument();
expect(screen.getByText('已选择 1 辆')).toBeInTheDocument(); expect(screen.getByText('已选择 1 辆')).toBeInTheDocument();
expect(screen.getByText('已绑定主车辆').closest('.semi-card')).toHaveClass('v2-mileage-summary-card'); expect(screen.getByText('已绑定主车辆').closest('[role="listitem"]')).toHaveClass('is-primary', 'is-success');
expect(screen.getByText('车辆每日里程').closest('.semi-card')).toHaveClass('v2-mileage-results'); expect(screen.getByText('车辆每日里程').closest('.semi-card')).toHaveClass('v2-mileage-results');
expect(view.container.querySelector('.v2-mileage-query-panel.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-mileage-query-panel.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-discovery-shell > .v2-mileage-command-bar')).toBeInTheDocument(); expect(view.container.querySelector('.v2-mileage-discovery-shell > .v2-mileage-command-bar')).toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-page > .v2-page-heading')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-mileage-page > .v2-page-heading')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-summary.semi-card')).toHaveAttribute('aria-label', '里程查询统计信息'); expect(view.container.querySelector('.v2-mileage-summary')).toHaveClass('v2-workspace-metric-rail', 'is-queue');
expect(view.container.querySelector('.v2-mileage-summary-secondary.semi-card-group')).toBeInTheDocument(); expect(view.container.querySelector('.v2-mileage-summary .v2-workspace-metric-list')).toHaveAttribute('aria-label', '里程查询统计信息');
expect(view.container.querySelectorAll('.v2-mileage-summary-card.semi-card')).toHaveLength(4); expect(view.container.querySelectorAll('.v2-mileage-summary .v2-workspace-metric-list > span')).toHaveLength(4);
expect(view.container.querySelector('.v2-mileage-summary-card.is-primary .v2-mileage-summary-value')).toHaveTextContent('193.3 km'); expect(view.container.querySelector('.v2-mileage-summary .v2-workspace-metric-list > span.is-primary')).toHaveTextContent('区间总里程193.3 km2 条车辆日记录');
expect(view.container.querySelector('.v2-mileage-summary .v2-workspace-metric-list')).toHaveTextContent('已绑定主车辆1 辆已选择 1 辆');
expect(view.container.querySelector('.v2-mileage-summary .v2-workspace-metric-list')).toHaveTextContent('统计天数2 天2026/07/13 — 07/14');
expect(view.container.querySelector('.v2-mileage-summary .v2-workspace-metric-list')).toHaveTextContent('日均里程96.7 km按有效车辆日平均');
expect(view.container.querySelector('.v2-mileage-table-wrap')).toBeInTheDocument(); expect(view.container.querySelector('.v2-mileage-table-wrap')).toBeInTheDocument();
expect(screen.getByRole('note', { name: '里程矩阵读表说明' })).toHaveTextContent('0 km已上报、无里程增量'); expect(screen.getByRole('note', { name: '里程矩阵读表说明' })).toHaveTextContent('0 km已上报、无里程增量');
expect(screen.getByRole('note', { name: '里程矩阵读表说明' })).toHaveTextContent('—无可用里程'); expect(screen.getByRole('note', { name: '里程矩阵读表说明' })).toHaveTextContent('—无可用里程');

View File

@@ -16,7 +16,7 @@ import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail'; import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
@@ -253,27 +253,42 @@ function SummaryRail({ data, criteria, fleetTotal, loading }: { data?: MileageSt
const summaryRange = criteria.dateFrom.slice(0, 4) === criteria.dateTo.slice(0, 4) const summaryRange = criteria.dateFrom.slice(0, 4) === criteria.dateTo.slice(0, 4)
? `${criteria.dateFrom.replace(/-/g, '/')}${criteria.dateTo.slice(5).replace('-', '/')}` ? `${criteria.dateFrom.replace(/-/g, '/')}${criteria.dateTo.slice(5).replace('-', '/')}`
: `${criteria.dateFrom.replace(/-/g, '/')}${criteria.dateTo.replace(/-/g, '/')}`; : `${criteria.dateFrom.replace(/-/g, '/')}${criteria.dateTo.replace(/-/g, '/')}`;
const items = loading ? [ const withUnit = (value: string | number, unit: string) => <>{value}<i className="v2-mileage-summary-unit"> {unit}</i></>;
['已绑定主车辆', '—', '正在按新筛选范围查询'], const items: WorkspaceQueueMetricRailItem[] = [
['统计天数', `${days}`, summaryRange], {
['区间总里程', '—', '正在计算区间汇总'], label: '区间总里程',
['日均里程', '—', '正在计算有效车辆日'] value: loading ? '—' : withUnit(formatKm(data?.periodMileageKm), 'km'),
] : [ note: loading ? '正在计算区间汇总' : `${data?.recordCount ?? 0} 条车辆日记录`,
['已绑定主车辆', `${vehicleCount}`, criteria.vehicles.length ? `已选择 ${criteria.vehicles.length}` : `档案口径 · ${data?.vehicleCount ?? 0} 辆有里程`], tone: 'primary',
['统计天数', `${days}`, summaryRange], emphasis: 'primary'
['区间总里程', `${formatKm(data?.periodMileageKm)} km`, `${data?.recordCount ?? 0} 条车辆日记录`], },
['日均里程', `${formatKm(data?.averageDailyMileageKm)} km`, '按有效车辆日平均'] {
label: '已绑定主车辆',
value: loading ? '—' : withUnit(vehicleCount, '辆'),
note: loading ? '正在按新筛选范围查询' : criteria.vehicles.length ? `已选择 ${criteria.vehicles.length}` : `档案口径 · ${data?.vehicleCount ?? 0} 辆有里程`,
tone: 'success',
emphasis: 'primary'
},
{
label: '统计天数',
value: withUnit(days, '天'),
note: summaryRange,
tone: 'neutral',
emphasis: 'secondary'
},
{
label: '日均里程',
value: loading ? '—' : withUnit(formatKm(data?.averageDailyMileageKm), 'km'),
note: loading ? '正在计算有效车辆日' : '按有效车辆日平均',
tone: 'primary',
emphasis: 'secondary'
}
]; ];
const primary = items[2];
const secondary = [items[0], items[1], items[3]];
return <WorkspaceMetricRail return <WorkspaceMetricRail
variant="queue"
className="v2-mileage-summary" className="v2-mileage-summary"
cardClassName="v2-mileage-summary-card"
secondaryClassName="v2-mileage-summary-secondary"
primaryValueClassName="v2-mileage-summary-value"
ariaLabel="里程查询统计信息" ariaLabel="里程查询统计信息"
primary={{ label: primary[0], value: primary[1], note: primary[2] }} items={items}
secondary={secondary.map(([label, value, note], index) => ({ label, value, note, className: `is-${['fleet', 'range', 'average'][index]}` }))}
/>; />;
} }

View File

@@ -430,9 +430,11 @@ describe('V2 production entry', () => {
expect(corePageSources.StatisticsPage).toContain('<Input'); expect(corePageSources.StatisticsPage).toContain('<Input');
expect(corePageSources.StatisticsPage).toContain('className="v2-mileage-query-panel"'); expect(corePageSources.StatisticsPage).toContain('className="v2-mileage-query-panel"');
expect(corePageSources.StatisticsPage).toContain('<WorkspaceMetricRail'); expect(corePageSources.StatisticsPage).toContain('<WorkspaceMetricRail');
expect(corePageSources.StatisticsPage).toContain('variant="queue"');
expect(corePageSources.StatisticsPage).toContain('className="v2-mileage-summary"'); expect(corePageSources.StatisticsPage).toContain('className="v2-mileage-summary"');
expect(corePageSources.StatisticsPage).toContain('secondaryClassName="v2-mileage-summary-secondary"'); expect(corePageSources.StatisticsPage).toContain('items={items}');
expect(corePageSources.StatisticsPage).toContain('cardClassName="v2-mileage-summary-card"'); expect(corePageSources.StatisticsPage).not.toContain('secondaryClassName="v2-mileage-summary-secondary"');
expect(corePageSources.StatisticsPage).not.toContain('cardClassName="v2-mileage-summary-card"');
expect(corePageSources.StatisticsPage).toContain('<Card key={source.protocol} className={`v2-mileage-source-card'); expect(corePageSources.StatisticsPage).toContain('<Card key={source.protocol} className={`v2-mileage-source-card');
expect(corePageSources.StatisticsPage).not.toContain('<section className="v2-mileage-summary"'); expect(corePageSources.StatisticsPage).not.toContain('<section className="v2-mileage-summary"');
expect(corePageSources.StatisticsPage).not.toContain('<article key={source.protocol}'); expect(corePageSources.StatisticsPage).not.toContain('<article key={source.protocol}');

View File

@@ -39,7 +39,7 @@ describe('WorkspaceMetricRail', () => {
/>); />);
const rail = view.container.querySelector('.v2-workspace-metric-rail'); const rail = view.container.querySelector('.v2-workspace-metric-rail');
expect(rail).toHaveClass('semi-card', 'is-queue', 'queue-metrics'); expect(rail).toHaveClass('semi-card', 'is-queue', 'has-context', 'queue-metrics');
const list = screen.getByRole('list', { name: '处置队列概览' }); const list = screen.getByRole('list', { name: '处置队列概览' });
expect(within(list).getAllByRole('listitem')).toHaveLength(4); expect(within(list).getAllByRole('listitem')).toHaveLength(4);
expect(within(list).getByText('待复核').closest('[role="listitem"]')).toHaveClass('is-primary'); expect(within(list).getByText('待复核').closest('[role="listitem"]')).toHaveClass('is-primary');
@@ -70,6 +70,7 @@ describe('WorkspaceMetricRail', () => {
/>); />);
const list = screen.getByRole('list', { name: '告警队列概览' }); const list = screen.getByRole('list', { name: '告警队列概览' });
expect(list.closest('.v2-workspace-metric-rail')).not.toHaveClass('has-context');
const action = within(list).getByRole('button', { name: '筛选未处理,共 687 条' }); const action = within(list).getByRole('button', { name: '筛选未处理,共 687 条' });
expect(action).toHaveClass('semi-button', 'v2-workspace-metric-action'); expect(action).toHaveClass('semi-button', 'v2-workspace-metric-action');
expect(action).toHaveAttribute('aria-pressed', 'true'); expect(action).toHaveAttribute('aria-pressed', 'true');

View File

@@ -39,7 +39,8 @@ type WorkspaceQueueMetricRailProps = {
export function WorkspaceMetricRail(props: WorkspaceSummaryMetricRailProps | WorkspaceQueueMetricRailProps) { export function WorkspaceMetricRail(props: WorkspaceSummaryMetricRailProps | WorkspaceQueueMetricRailProps) {
if ('items' in props) { if ('items' in props) {
return <Card className={`v2-workspace-metric-rail is-queue ${props.className}`} bodyStyle={{ padding: 0 }}> const hasContext = props.context != null;
return <Card className={`v2-workspace-metric-rail is-queue${hasContext ? ' has-context' : ''} ${props.className}`} bodyStyle={{ padding: 0 }}>
<div className="v2-workspace-metric-list" role="list" aria-label={props.ariaLabel}> <div className="v2-workspace-metric-list" role="list" aria-label={props.ariaLabel}>
{props.items.map((item, index) => { {props.items.map((item, index) => {
const toneClassName = `is-${item.tone ?? 'neutral'}`; const toneClassName = `is-${item.tone ?? 'neutral'}`;
@@ -72,7 +73,7 @@ export function WorkspaceMetricRail(props: WorkspaceSummaryMetricRailProps | Wor
</span>; </span>;
})} })}
</div> </div>
{props.context ? <div className="v2-workspace-metric-context">{props.context}</div> : null} {hasContext ? <div className="v2-workspace-metric-context">{props.context}</div> : null}
</Card>; </Card>;
} }

View File

@@ -15646,6 +15646,10 @@
.v2-workspace-metric-rail.is-queue > .semi-card-body { .v2-workspace-metric-rail.is-queue > .semi-card-body {
display: grid; display: grid;
min-height: 58px; min-height: 58px;
grid-template-columns: minmax(0, 1fr);
}
.v2-workspace-metric-rail.is-queue.has-context > .semi-card-body {
grid-template-columns: minmax(0, 1fr) 218px; grid-template-columns: minmax(0, 1fr) 218px;
} }
@@ -15942,7 +15946,7 @@
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.v2-workspace-metric-rail.is-queue > .semi-card-body { .v2-workspace-metric-rail.is-queue.has-context > .semi-card-body {
grid-template-columns: minmax(0, 1fr) 180px; grid-template-columns: minmax(0, 1fr) 180px;
} }
@@ -23218,3 +23222,72 @@
display: none; display: none;
} }
} }
/*
* Mileage analysis now uses the shared Semi UI metric rail. These late
* overrides retire the former hero-card dimensions without carrying their
* fixed heights into the compact queue layout.
*/
.v2-mileage-summary.v2-workspace-metric-rail.is-queue.semi-card {
display: block;
width: 100%;
height: auto;
min-height: 0;
gap: 0;
overflow: hidden;
border: 0;
border-top: 1px solid #e2e9f2;
border-radius: 0 0 11px 11px;
background: #fff;
box-shadow: none;
}
.v2-mileage-summary.v2-workspace-metric-rail.is-queue > .semi-card-body {
display: grid;
height: auto;
min-height: 58px;
grid-template-columns: minmax(0, 1fr);
gap: 0;
}
.v2-mileage-summary .v2-workspace-metric-list {
grid-template-columns: 1.3fr 1.1fr .9fr .9fr;
}
.v2-mileage-summary .v2-workspace-metric-list > span {
display: grid;
min-height: 58px;
}
.v2-mileage-summary-unit {
margin-left: 2px;
color: #8493a5;
font-size: 8px;
font-style: normal;
font-weight: 600;
letter-spacing: 0;
}
.v2-mileage-summary .v2-workspace-metric-list > span.is-primary .v2-mileage-summary-unit {
color: #6f90bb;
}
.v2-mileage-summary .v2-workspace-metric-list > span.is-success .v2-mileage-summary-unit {
color: #5b9c7f;
}
@media (max-width: 680px) {
.v2-mileage-summary.v2-workspace-metric-rail.is-queue.semi-card,
.v2-mileage-summary.v2-workspace-metric-rail.is-queue > .semi-card-body {
height: auto;
min-height: 56px;
}
.v2-mileage-summary .v2-workspace-metric-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.v2-mileage-summary .v2-workspace-metric-list > span {
min-height: 56px;
}
}