feat(web): refine track and mileage workspaces

This commit is contained in:
lingniu
2026-07-20 05:12:12 +08:00
parent 9eb77a2d57
commit c150e45f11
6 changed files with 47 additions and 30 deletions

View File

@@ -55,9 +55,8 @@ test('renders only the desktop matrix with dates as columns and a period total',
expect(screen.getAllByText('7/13').length).toBeGreaterThan(0);
expect(screen.getAllByText('7/14').length).toBeGreaterThan(0);
expect(screen.getAllByText('粤A12345').length).toBeGreaterThan(0);
expect(screen.getByLabelText('里程查询工作区')).toHaveClass('v2-workspace-command-bar', 'v2-mileage-command-bar');
expect(screen.getByRole('heading', { name: '车辆里程分析', level: 5 })).toBeInTheDocument();
expect(screen.getByLabelText('里程查询工作区')).toHaveTextContent('GB32960 优先');
expect(screen.queryByLabelText('里程查询工作区')).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: '车辆里程分析' })).not.toBeInTheDocument();
expect(screen.getByRole('heading', { name: '里程范围', level: 5 })).toBeInTheDocument();
expect(screen.getByText('车牌、自然日与里程数据源策略')).toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-date-range.semi-datepicker')).toBeInTheDocument();
@@ -74,7 +73,7 @@ test('renders only the desktop matrix with dates as columns and a period total',
expect(screen.getByText('已绑定主车辆').closest('[role="listitem"]')).toHaveClass('is-primary', 'is-success');
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-discovery-shell > .v2-mileage-command-bar')).toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-discovery-shell > .v2-mileage-command-bar')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-page > .v2-page-heading')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-mileage-summary')).toHaveClass('v2-workspace-metric-rail', 'is-queue');
expect(view.container.querySelector('.v2-mileage-summary .v2-workspace-metric-list')).toHaveAttribute('aria-label', '里程查询统计信息');

View File

@@ -15,7 +15,6 @@ import { TablePagination } from '../shared/TablePagination';
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
@@ -705,15 +704,6 @@ export default function StatisticsPage() {
return <div className="v2-mileage-page" ref={pageRef}>
<MonitorReturnBar />
<div className="v2-mileage-discovery-shell">
<WorkspaceCommandBar
className="v2-mileage-command-bar"
ariaLabel="里程查询工作区"
title="车辆里程分析"
description="按自然日对比车辆行驶里程与协议来源"
status={`${criteria.vehicles.length ? `${criteria.vehicles.length}` : '全部授权车辆'} · ${inclusiveDays(criteria.dateFrom, criteria.dateTo)}`}
statusColor={criteria.vehicles.length ? 'blue' : 'grey'}
meta={<span>{criteria.sources.find((source) => source.enabled)?.protocol ?? '未配置'} </span>}
/>
{mobileLayout ? <div className="v2-mileage-mobile-discovery">
<MobileFilterToggle
title="里程范围"

View File

@@ -225,7 +225,7 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
fireEvent.click(screen.getByRole('tab', { name: '概览' }));
expect(view.container.querySelectorAll('.v2-track-overview-section')).toHaveLength(3);
expect(view.container.querySelectorAll('.v2-track-overview-section.semi-card')).toHaveLength(0);
expect(view.container.querySelectorAll('.v2-track-overview-section.semi-card')).toHaveLength(3);
expect(view.container.querySelector('.v2-track-overview-descriptions.semi-descriptions')).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-source-list.semi-list')).toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-track-source-item.semi-list-item')).toHaveLength(1);

View File

@@ -11,7 +11,7 @@ import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../.
import { protocolDisplayLabel } from '../domain/protocols';
import { buildTrackRailSelection, compactTrackWindowLabel, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, trackPointMileageAvailable, type TrackAddressCoordinate } from '../domain/track';
import { TrackMap } from '../map/TrackMap';
import { InlineError, PanelEmpty, WorkspaceEmptyGuide } from '../shared/AsyncState';
import { InlineError, PanelEmpty, PanelLoading, WorkspaceEmptyGuide } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { ProtocolTag } from '../shared/ProtocolTag';
import { SegmentedTabs } from '../shared/SegmentedTabs';
@@ -210,7 +210,7 @@ function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange:
function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
const distanceLabel = track.summary.distanceMethod === 'coordinates' ? 'GPS 轨迹累计' : track.summary.distanceMethod === 'odometer' ? '协议总里程差' : '行驶里程';
return <div className="v2-track-overview-panel">
<section className="v2-track-overview-section">
<Card className="v2-track-overview-section v2-track-overview-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader variant="compact" title="行程概览" meta={<Tag color={track.sampled ? 'orange' : 'green'} type="light" size="small">{track.sampled ? '地图已抽稀' : '完整点集'}</Tag>} />
<div className="v2-track-overview-card-body"><Descriptions className="v2-track-overview-descriptions" align="left" size="small" data={[
{ key: '开始时间', value: dateTime(track.summary.startTime) },
@@ -220,15 +220,15 @@ function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
{ key: '平均 / 最高速度', value: `${number(track.summary.averageSpeedKmh)} / ${number(track.summary.maximumSpeedKmh)} km/h` },
{ key: '停车 / 分段', value: `${track.summary.stopCount} / ${track.summary.segmentCount}` }
]} /></div>
</section>
<section className="v2-track-overview-section v2-track-source-card">
</Card>
<Card className="v2-track-overview-section v2-track-overview-card v2-track-source-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader variant="compact" title="数据来源" meta={<Tag color="blue" type="light" size="small">{track.coverage.totalPoints.toLocaleString('zh-CN')} </Tag>} />
<div className="v2-track-overview-card-body"><List className="v2-track-source-list">{track.sources.map((source) => <List.Item className="v2-track-source-item" key={source.protocol}><ProtocolTag className="v2-track-protocol-tag" protocol={source.protocol} compact /><strong>{source.pointCount.toLocaleString('zh-CN')} </strong><small>{time(source.startTime)}{time(source.endTime)}</small></List.Item>)}</List></div>
</section>
<section className={`v2-track-overview-section v2-track-quality-card is-${track.quality.status}`}>
</Card>
<Card className={`v2-track-overview-section v2-track-overview-card v2-track-quality-card is-${track.quality.status}`} bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader variant="compact" title="轨迹质量" meta={<Tag color={track.quality.status === 'good' ? 'green' : 'orange'} type="light" size="small">{track.quality.status === 'good' ? '通过' : '需关注'}</Tag>} />
<div className="v2-track-overview-card-body"><p>{track.quality.evidence}</p><small> {track.quality.invalidCoordinatePoints} · {track.quality.duplicatePoints} · {track.quality.driftPoints} · {track.quality.largeGapCount}</small></div>
</section>
</Card>
</div>;
}
@@ -538,7 +538,7 @@ export default function TrackPage() {
action={<Button theme="solid" icon={<IconSearch />} onClick={() => setRailCollapsed(false)}></Button>}
/></Card>}
{query.isFetching ? <div className="v2-track-loading"><span className="v2-spinner" /></div> : null}
{query.isFetching ? <PanelLoading className="v2-track-loading" compact title="正在生成轨迹" description="正在校验来源覆盖并整理停留与事件证据。" /> : null}
{query.isError ? <div className="v2-track-error"><InlineError message={query.error instanceof Error ? query.error.message : '轨迹查询失败'} onRetry={() => query.refetch()} /></div> : null}
<Card className="v2-track-playback-dock" bodyStyle={{ padding: 0 }}>

View File

@@ -69,7 +69,7 @@ describe('V2 production entry', () => {
expect(corePageSources[name]).not.toContain("from '../shared/PageHeader'");
expect(corePageSources[name]).not.toContain('<PageHeader');
}
for (const name of ['VehiclePage', 'TrackPage', 'AccessPage', 'OperationsPage', 'StatisticsPage', 'UsersPage']) {
for (const name of ['VehiclePage', 'TrackPage', 'AccessPage', 'OperationsPage', 'UsersPage']) {
expect(corePageSources[name]).toContain("from '../shared/WorkspaceCommandBar'");
expect(corePageSources[name]).toContain('<WorkspaceCommandBar');
}
@@ -446,7 +446,7 @@ describe('V2 production entry', () => {
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-current-card"');
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-empty-state"');
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-playback-dock"');
expect(corePageSources.TrackPage).toContain('<section className="v2-track-overview-section">');
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-overview-section v2-track-overview-card"');
expect(corePageSources.TrackPage).toContain('<Descriptions className="v2-track-overview-descriptions"');
expect(corePageSources.TrackPage).toContain('<PanelEmpty className="v2-track-rail-empty"');
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-stop-list"');
@@ -462,12 +462,14 @@ describe('V2 production entry', () => {
expect(corePageSources.TrackPage).toContain("primaryAction={{ label: '查看地图'");
expect(corePageSources.TrackPage).not.toContain('useSideSheetA11y(mobileLayout');
expect(corePageSources.TrackPage).not.toContain('<Card className="v2-track-overview-card"><header><strong>行程概览</strong><Tag color={track.sampled ? \'orange\' : \'green\'} type="light" size="small">{track.sampled ? \'地图已抽稀\' : \'完整点集\'}</Tag></header><dl>');
expect(corePageSources.TrackPage).toContain('<section className="v2-track-overview-section v2-track-source-card">');
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-overview-section v2-track-overview-card v2-track-source-card"');
expect(corePageSources.TrackPage).toContain('<List className="v2-track-source-list"');
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-source-item"');
expect(corePageSources.TrackPage).not.toContain('<Card className="v2-track-source-item"');
expect(corePageSources.TrackPage).toContain('<section className={`v2-track-overview-section v2-track-quality-card');
expect(corePageSources.TrackPage).not.toContain('<section className={`v2-track-quality-card');
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-overview-section v2-track-overview-card"');
expect(corePageSources.TrackPage).toContain('<Card className={`v2-track-overview-section v2-track-overview-card v2-track-quality-card');
expect(corePageSources.TrackPage).toContain('<PanelLoading className="v2-track-loading"');
expect(corePageSources.TrackPage).not.toContain('<span className="v2-spinner"');
expect(corePageSources.TrackPage).not.toContain('<article key={source.protocol}');
expect(corePageSources.TrackPage).not.toContain('<article className="v2-track-current-card"');
expect(corePageSources.TrackPage).not.toContain('<footer className="v2-track-playback-dock"');
@@ -476,6 +478,9 @@ describe('V2 production entry', () => {
expect(corePageSources.TrackPage).not.toContain('<input');
expect(corePageSources.TrackPage).not.toContain('<button');
expect(corePageSources.StatisticsPage).toContain('<Input');
expect(corePageSources.StatisticsPage).not.toContain("from '../shared/WorkspaceCommandBar'");
expect(corePageSources.StatisticsPage).not.toContain('<WorkspaceCommandBar');
expect(corePageSources.StatisticsPage).not.toContain('title="车辆里程分析"');
expect(corePageSources.StatisticsPage).toContain('className="v2-mileage-query-panel"');
expect(corePageSources.StatisticsPage).toContain('<WorkspaceMetricRail');
expect(corePageSources.StatisticsPage).toContain('variant="queue"');

View File

@@ -15033,7 +15033,7 @@
width: 100%;
overflow: visible;
border: 0;
border-radius: 0 0 12px 12px;
border-radius: 12px;
box-shadow: none;
}
@@ -24142,6 +24142,23 @@
display: none;
}
.v2-track-loading.v2-state-surface {
width: min(330px, calc(100% - 32px));
min-height: 82px;
height: auto;
align-items: stretch;
border-radius: 12px;
padding: 10px 12px;
}
.v2-track-loading .v2-state-message {
min-height: 40px;
}
.v2-track-loading .v2-state-skeleton {
display: none;
}
@media (max-width: 680px) {
.v2-track-detail-sidesheet .v2-workspace-config-summary {
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -24234,7 +24251,7 @@
.v2-track-current-card.semi-card {
top: 12px;
left: 12px;
width: min(404px, calc(100% - 350px));
width: min(440px, calc(100% - 280px));
border-radius: 13px;
box-shadow: 0 14px 38px rgba(26, 47, 73, .14);
}
@@ -24252,6 +24269,12 @@
line-height: 20px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span:last-child strong {
overflow: visible;
font-size: clamp(13px, 1.1vw, 15px);
text-overflow: clip;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-evidence {
min-height: 40px;
}