refine Semi UI track and vehicle workspaces
This commit is contained in:
@@ -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(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-rail-result')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { QUERY_MEMORY } from '../queryPolicy';
|
||||
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">
|
||||
<Card className="v2-track-rail-shell" bodyStyle={{ padding: 0 }}>
|
||||
<section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}`}>
|
||||
<WorkspacePanelHeader
|
||||
<WorkspaceCommandBar
|
||||
className="v2-track-query-header"
|
||||
ariaLabel="轨迹查询任务栏"
|
||||
title="轨迹查询"
|
||||
description={queryCollapsed ? `${appliedWindow} · ${appliedSource}` : '最长支持连续 7 天'}
|
||||
status={loading ? '读取中' : track ? `${track.points.length.toLocaleString('zh-CN')} 点` : '等待查询'}
|
||||
statusColor={loading ? 'blue' : track ? 'green' : 'grey'}
|
||||
actions={<>
|
||||
<Button
|
||||
className="v2-track-query-toggle"
|
||||
|
||||
@@ -79,6 +79,9 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
|
||||
]));
|
||||
const commandCard = view.container.querySelector('.v2-identity-band');
|
||||
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(commandCard?.querySelectorAll('.v2-live-grid > [role="listitem"]')).toHaveLength(6);
|
||||
expect(commandCard?.querySelector('.v2-live-grid > div:first-child')).toHaveTextContent('速度');
|
||||
|
||||
@@ -20,6 +20,7 @@ import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
||||
import { PlatformTime } from '../shared/PlatformTime';
|
||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext';
|
||||
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)} 车辆档案`}>
|
||||
<MonitorReturnBar />
|
||||
<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">
|
||||
<div className="v2-identity-primary">
|
||||
<WorkspaceCommandBar
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
<div className="v2-identity-meta">
|
||||
<div><small>车辆品牌 / 车型</small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||
</span>}
|
||||
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>}
|
||||
meta={<span className="v2-identity-meta">
|
||||
<span className="v2-identity-model"><small>车辆品牌 / 车型</small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></span>
|
||||
<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>
|
||||
</span>}
|
||||
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>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
<section className="v2-vehicle-live-section" aria-labelledby="vehicle-live-heading">
|
||||
<WorkspacePanelHeader
|
||||
title={<span id="vehicle-live-heading">最新上报</span>}
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources[name]).not.toContain("from '../shared/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('<WorkspaceCommandBar');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,388 @@
|
||||
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
|
||||
* the only in-flow workspace child. Bind layout to the component's mobile
|
||||
|
||||
Reference in New Issue
Block a user