refine Semi UI track and vehicle workspaces

This commit is contained in:
lingniu
2026-07-18 14:34:01 +08:00
parent 01d165d771
commit c57caabaa0
6 changed files with 407 additions and 13 deletions

View File

@@ -173,6 +173,8 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0); expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
expect(view.container.querySelector('.v2-track-rail-shell.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-track-rail-shell.semi-card')).toBeInTheDocument();
expect(screen.getByLabelText('轨迹查询任务栏')).toHaveClass('v2-workspace-command-bar', 'v2-track-query-header');
expect(screen.getByLabelText('轨迹查询任务栏')).toHaveTextContent('3 点');
expect(view.container.querySelector('.v2-track-query-panel')).toHaveClass('is-collapsed'); expect(view.container.querySelector('.v2-track-query-panel')).toHaveClass('is-collapsed');
expect(view.container.querySelector('.v2-track-rail-result')).toBeInTheDocument(); expect(view.container.querySelector('.v2-track-rail-result')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false'); expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');

View File

@@ -15,6 +15,7 @@ import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { SegmentedTabs } from '../shared/SegmentedTabs'; import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { QUERY_MEMORY } from '../queryPolicy'; import { QUERY_MEMORY } from '../queryPolicy';
import { monitorReturnFromParams, preserveMonitorReturn } from '../routing/monitorContext'; import { monitorReturnFromParams, preserveMonitorReturn } from '../routing/monitorContext';
@@ -197,10 +198,13 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
return <aside className="v2-track-rail"> return <aside className="v2-track-rail">
<Card className="v2-track-rail-shell" bodyStyle={{ padding: 0 }}> <Card className="v2-track-rail-shell" bodyStyle={{ padding: 0 }}>
<section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}`}> <section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}`}>
<WorkspacePanelHeader <WorkspaceCommandBar
className="v2-track-query-header" className="v2-track-query-header"
ariaLabel="轨迹查询任务栏"
title="轨迹查询" title="轨迹查询"
description={queryCollapsed ? `${appliedWindow} · ${appliedSource}` : '最长支持连续 7 天'} description={queryCollapsed ? `${appliedWindow} · ${appliedSource}` : '最长支持连续 7 天'}
status={loading ? '读取中' : track ? `${track.points.length.toLocaleString('zh-CN')}` : '等待查询'}
statusColor={loading ? 'blue' : track ? 'green' : 'grey'}
actions={<> actions={<>
<Button <Button
className="v2-track-query-toggle" className="v2-track-query-toggle"

View File

@@ -79,6 +79,9 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
])); ]));
const commandCard = view.container.querySelector('.v2-identity-band'); const commandCard = view.container.querySelector('.v2-identity-band');
expect(commandCard).toHaveClass('semi-card', 'v2-vehicle-command-card', 'v2-live-overview'); expect(commandCard).toHaveClass('semi-card', 'v2-vehicle-command-card', 'v2-live-overview');
expect(screen.getByLabelText('单车任务栏')).toHaveClass('v2-workspace-command-bar', 'v2-vehicle-record-command');
expect(screen.getByLabelText('单车任务栏')).toHaveTextContent('粤A12345');
expect(screen.getByLabelText('单车任务栏')).toHaveTextContent('LTEST000000000001');
expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-live-grid'); expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-live-grid');
expect(commandCard?.querySelectorAll('.v2-live-grid > [role="listitem"]')).toHaveLength(6); expect(commandCard?.querySelectorAll('.v2-live-grid > [role="listitem"]')).toHaveLength(6);
expect(commandCard?.querySelector('.v2-live-grid > div:first-child')).toHaveTextContent('速度'); expect(commandCard?.querySelector('.v2-live-grid > div:first-child')).toHaveTextContent('速度');

View File

@@ -20,6 +20,7 @@ import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { PlatformTime } from '../shared/PlatformTime'; import { PlatformTime } from '../shared/PlatformTime';
import { SegmentedTabs } from '../shared/SegmentedTabs'; import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext'; import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext';
import { useMobileLayout } from '../hooks/useMobileLayout'; import { useMobileLayout } from '../hooks/useMobileLayout';
@@ -456,20 +457,22 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
return <div ref={recordRootRef} className={`v2-vehicle-record-page${mobileLayout ? ' is-mobile-layout' : ''}`} tabIndex={-1} aria-label={`${fmt(identity?.plate || realtime?.plate)} 车辆档案`}> return <div ref={recordRootRef} className={`v2-vehicle-record-page${mobileLayout ? ' is-mobile-layout' : ''}`} tabIndex={-1} aria-label={`${fmt(identity?.plate || realtime?.plate)} 车辆档案`}>
<MonitorReturnBar /> <MonitorReturnBar />
<Card className="v2-identity-band v2-record-card v2-live-card v2-live-overview v2-vehicle-command-card" bodyStyle={{ padding: 0 }}> <Card className="v2-identity-band v2-record-card v2-live-card v2-live-overview v2-vehicle-command-card" bodyStyle={{ padding: 0 }}>
<div className="v2-vehicle-command-header"> <WorkspaceCommandBar
<div className="v2-identity-primary"> className="v2-vehicle-command-header v2-vehicle-record-command"
ariaLabel="单车任务栏"
title={<span className="v2-identity-primary">
<span className="v2-plate"><IconBox />{fmt(identity?.plate || realtime?.plate)}</span> <span className="v2-plate"><IconBox />{fmt(identity?.plate || realtime?.plate)}</span>
<Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag> <Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag>
<span className="v2-identity-vin"><small>VIN</small><b>{detail.vin}</b><Button theme="borderless" aria-label="复制 VIN" title="复制 VIN" icon={<IconCopy />} onClick={() => navigator.clipboard?.writeText(detail.vin)} /></span> </span>}
</div> description={<span className="v2-identity-vin"><small>VIN</small><b>{detail.vin}</b><Button theme="borderless" aria-label="复制 VIN" title="复制 VIN" icon={<IconCopy />} onClick={() => navigator.clipboard?.writeText(detail.vin)} /></span>}
<div className="v2-identity-meta"> meta={<span className="v2-identity-meta">
<div><small> / </small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></div> <span className="v2-identity-model"><small> / </small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></span>
<div><small></small><div className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <Tag key={source} color="blue" type="light" size="small">{source}</Tag>) : <Tag color="grey" type="light" size="small"></Tag>}</div></div> <span className="v2-identity-source-context"><small></small><span className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <Tag key={source} color="blue" type="light" size="small">{source}</Tag>) : <Tag color="grey" type="light" size="small"></Tag>}</span></span>
</div> </span>}
<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作"> actions={<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)} {actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)}
</div> </div>}
</div> />
<section className="v2-vehicle-live-section" aria-labelledby="vehicle-live-heading"> <section className="v2-vehicle-live-section" aria-labelledby="vehicle-live-heading">
<WorkspacePanelHeader <WorkspacePanelHeader
title={<span id="vehicle-live-heading"></span>} title={<span id="vehicle-live-heading"></span>}

View File

@@ -63,7 +63,7 @@ describe('V2 production entry', () => {
expect(corePageSources[name]).not.toContain("from '../shared/PageHeader'"); expect(corePageSources[name]).not.toContain("from '../shared/PageHeader'");
expect(corePageSources[name]).not.toContain('<PageHeader'); expect(corePageSources[name]).not.toContain('<PageHeader');
} }
for (const name of ['AccessPage', 'OperationsPage', 'UsersPage']) { for (const name of ['VehiclePage', 'TrackPage', 'AccessPage', 'OperationsPage', 'UsersPage']) {
expect(corePageSources[name]).toContain("from '../shared/WorkspaceCommandBar'"); expect(corePageSources[name]).toContain("from '../shared/WorkspaceCommandBar'");
expect(corePageSources[name]).toContain('<WorkspaceCommandBar'); expect(corePageSources[name]).toContain('<WorkspaceCommandBar');
} }

View File

@@ -18,6 +18,388 @@
padding: 8px 14px; padding: 8px 14px;
} }
/*
* Semi UI task-bar convergence for trajectory replay and single-vehicle
* investigation. These routes keep their map-first information density while
* sharing the same command anatomy as the directory and operations workspaces.
*/
.v2-track-query-panel .v2-track-query-header.v2-workspace-command-bar {
width: 100%;
max-width: none;
height: auto;
min-height: 58px;
margin: 0;
border: 0;
border-radius: 0;
padding: 8px 8px 8px 12px;
box-shadow: none;
}
.v2-track-query-header .v2-workspace-command-copy {
gap: 1px;
}
.v2-track-query-header .v2-workspace-command-copy > .semi-typography:first-child {
color: #26384f;
font-size: 16px;
font-weight: 750;
line-height: 20px;
}
.v2-track-query-header .v2-workspace-command-copy > .semi-typography:last-child {
color: #8491a2;
font-size: 10px;
line-height: 14px;
}
.v2-track-query-header .v2-workspace-command-actions {
gap: 3px;
}
.v2-track-query-header .v2-workspace-command-actions > .semi-tag {
min-height: 22px;
padding-inline: 7px;
font-size: 9px;
}
.v2-track-query-header .v2-workspace-command-actions > .semi-button {
flex: 0 0 auto;
}
.v2-vehicle-record-command.v2-workspace-command-bar {
display: grid;
width: 100%;
max-width: none;
height: auto;
min-height: 96px;
grid-template-columns: minmax(260px, .7fr) minmax(0, 1.85fr);
align-items: stretch;
gap: 0;
margin: 0;
border: 0;
border-radius: 0;
padding: 0;
box-shadow: none;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy {
min-height: 96px;
align-content: center;
gap: 6px;
border-right: 1px solid #e4eaf2;
padding: 13px 16px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy > .semi-typography:first-child,
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy > .semi-typography:last-child {
display: block;
min-width: 0;
overflow: visible;
line-height: normal;
white-space: normal;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-primary {
display: flex;
min-width: 0;
align-items: center;
gap: 9px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-plate {
min-height: 38px;
padding-inline: 12px;
font-size: 16px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-online-label.semi-tag {
min-height: 24px;
flex: 0 0 auto;
margin: 0;
border-radius: 999px;
padding-inline: 8px;
font-size: 9px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-vin {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-vin small {
flex: 0 0 auto;
color: #8290a2;
font-size: 9px;
font-weight: 650;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-vin b {
min-width: 0;
overflow: hidden;
color: #41536a;
font-size: 10px;
font-variant-numeric: tabular-nums;
letter-spacing: .015em;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-vin .semi-button {
width: 26px;
min-width: 26px;
height: 26px;
flex: 0 0 auto;
border-radius: 7px;
color: #73849a;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-actions {
display: grid;
min-width: 0;
align-items: stretch;
grid-template-columns: minmax(280px, .9fr) minmax(430px, 1.1fr);
gap: 0;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-meta {
display: flex;
width: 100%;
min-width: 0;
align-items: stretch;
border-right: 1px solid #e4eaf2;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: minmax(150px, 1.08fr) minmax(135px, .92fr);
align-items: stretch;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta > span {
display: flex;
min-width: 0;
flex-direction: column;
justify-content: center;
padding: 11px 14px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta > span + span {
border-left: 1px solid #e7edf4;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta small {
color: #7b899b;
font-size: 9px;
font-weight: 650;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta strong {
display: block;
margin-top: 5px;
overflow: hidden;
color: #2c4057;
font-size: 11px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-sources {
display: flex;
min-width: 0;
flex-wrap: wrap;
gap: 4px;
margin-top: 5px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-sources .semi-tag {
min-height: 21px;
flex: 0 0 auto;
margin: 0;
border-radius: 6px;
padding-inline: 6px;
font-size: 8px;
font-weight: 650;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-content: center;
justify-content: flex-end;
gap: 5px;
padding: 10px 12px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button {
width: auto;
min-width: 82px;
min-height: 33px;
flex: 0 1 auto;
border-radius: 8px;
padding-inline: 9px;
font-size: 9px;
font-weight: 650;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button-primary {
border-color: #bcd3f8;
background: #edf4ff;
color: var(--v2-blue);
}
@media (max-width: 1080px) {
.v2-vehicle-record-command.v2-workspace-command-bar {
grid-template-columns: minmax(250px, .7fr) minmax(0, 1.3fr);
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-actions {
grid-template-columns: minmax(250px, .8fr) minmax(340px, 1.2fr);
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button {
min-width: 76px;
}
}
@media (max-width: 900px) and (min-width: 681px) {
.v2-vehicle-record-command.v2-workspace-command-bar {
grid-template-columns: 1fr;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy {
min-height: 74px;
border-right: 0;
border-bottom: 1px solid #e4eaf2;
padding-block: 10px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-actions {
grid-template-columns: minmax(280px, .8fr) minmax(0, 1.2fr);
}
}
@media (max-width: 680px) {
.v2-track-query-panel .v2-track-query-header.v2-workspace-command-bar {
min-height: 54px;
padding: 7px 8px 7px 12px;
}
.v2-track-query-header .v2-workspace-command-copy > .semi-typography:first-child {
font-size: 14px;
}
.v2-track-query-header .v2-workspace-command-actions > .semi-tag {
display: none;
}
.v2-vehicle-record-command.v2-workspace-command-bar {
grid-template-columns: 1fr;
min-height: 0;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy {
min-height: 68px;
gap: 4px;
border-right: 0;
border-bottom: 1px solid #e5ebf2;
padding: 8px 11px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy > .semi-typography:last-child {
display: block;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-plate {
min-height: 34px;
padding-inline: 10px;
font-size: 14px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-actions {
display: grid;
grid-template-columns: 1fr;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-meta {
display: flex;
border-right: 0;
border-bottom: 1px solid #e7edf4;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta {
grid-template-columns: minmax(0, 1fr) minmax(150px, auto);
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta > span {
min-height: 54px;
padding: 7px 10px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta strong {
margin-top: 3px;
font-size: 10px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-sources {
flex-wrap: nowrap;
gap: 3px;
overflow: hidden;
margin-top: 3px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-sources .semi-tag {
min-height: 19px;
padding-inline: 4px;
font-size: 7px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 4px;
overflow: visible;
padding: 5px 7px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button {
width: 100%;
min-width: 0;
min-height: 48px;
padding: 3px 1px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button .semi-button-content {
min-width: 0;
flex-direction: column;
justify-content: center;
gap: 2px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button .semi-button-content-left {
margin-right: 0;
font-size: 14px;
}
.v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button .semi-button-content-right {
max-width: 100%;
overflow: hidden;
margin-left: 0;
font-size: 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-vehicle-command-card .v2-live-source-link.semi-button {
padding-right: 0;
}
}
/* /*
* Track criteria live in a portal-backed SideSheet on phones, so the stage is * Track criteria live in a portal-backed SideSheet on phones, so the stage is
* the only in-flow workspace child. Bind layout to the component's mobile * the only in-flow workspace child. Bind layout to the component's mobile