feat: unify monitor vehicle metric rail
This commit is contained in:
@@ -170,11 +170,15 @@ test('starts without a selection and supports expand, collapse, reselection, and
|
|||||||
'单车详情', '轨迹回放', '历史数据', '里程查询'
|
'单车详情', '轨迹回放', '历史数据', '里程查询'
|
||||||
]);
|
]);
|
||||||
expect(view.container.querySelector('.v2-detail-protocols .v2-protocol-tag')).toHaveTextContent('JT/T 808');
|
expect(view.container.querySelector('.v2-detail-protocols .v2-protocol-tag')).toHaveTextContent('JT/T 808');
|
||||||
expect(view.container.querySelector('.v2-metric-grid > div:first-child')).toHaveTextContent('速度42');
|
expect(screen.getByRole('list', { name: '车辆实时状态指标' })).toHaveClass('v2-workspace-metric-list');
|
||||||
expect(view.container.querySelector('.v2-metric-grid > div:nth-child(4)')).toHaveTextContent('今日里程18.6km');
|
expect(view.container.querySelector('.v2-monitor-detail-metric-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue');
|
||||||
|
expect(view.container.querySelectorAll('.v2-monitor-detail-metric-rail .v2-workspace-metric-list > [role="listitem"]')).toHaveLength(4);
|
||||||
|
expect(view.container.querySelector('.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span:first-child')).toHaveTextContent('速度42');
|
||||||
|
expect(view.container.querySelector('.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span:nth-child(2)')).toHaveTextContent('今日里程18.6km');
|
||||||
|
expect(view.container.querySelector('.v2-monitor-detail-state')).toHaveTextContent('车辆状态行驶当前告警0条');
|
||||||
expect(view.container.querySelector('.v2-status-text')).toHaveClass('semi-tag');
|
expect(view.container.querySelector('.v2-status-text')).toHaveClass('semi-tag');
|
||||||
expect(screen.getByRole('button', { name: '取消选择车辆' })).toHaveClass('semi-button');
|
expect(screen.getByRole('button', { name: '取消选择车辆' })).toHaveClass('semi-button');
|
||||||
expect(screen.getByRole('button', { name: '查看总里程全部来源' })).toHaveClass('semi-button', 'v2-metric-source-link');
|
expect(screen.getByRole('button', { name: '查看总里程全部来源' })).toHaveClass('semi-button', 'v2-workspace-metric-action');
|
||||||
expect(screen.getByRole('button', { name: '查看全部位置来源' })).toHaveClass('semi-button', 'v2-detail-source-link');
|
expect(screen.getByRole('button', { name: '查看全部位置来源' })).toHaveClass('semi-button', 'v2-detail-source-link');
|
||||||
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', 'LTEST000000000001');
|
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', 'LTEST000000000001');
|
||||||
expect(vehicleCardArgsSpy).toHaveBeenLastCalledWith('LTEST000000000001', vehicles[0], true);
|
expect(vehicleCardArgsSpy).toHaveBeenLastCalledWith('LTEST000000000001', vehicles[0], true);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useMonitorVehicleCard } from '../hooks/useMonitorData';
|
|||||||
import { withMonitorReturn } from '../routing/monitorContext';
|
import { withMonitorReturn } from '../routing/monitorContext';
|
||||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||||
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
|
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
|
||||||
|
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
|
|
||||||
function hasRealtimeLocation(vehicle: VehicleRealtimeRow) {
|
function hasRealtimeLocation(vehicle: VehicleRealtimeRow) {
|
||||||
@@ -70,6 +71,40 @@ export default function MonitorVehicleDetailCard({
|
|||||||
'hydrogen_concentration_percent', 'hydrogen_pressure_mpa', 'hydrogen_temperature_c',
|
'hydrogen_concentration_percent', 'hydrogen_pressure_mpa', 'hydrogen_temperature_c',
|
||||||
'engine_speed_rpm', 'total_voltage_v', 'total_current_a'
|
'engine_speed_rpm', 'total_voltage_v', 'total_current_a'
|
||||||
].includes(item.key)).slice(0, 9);
|
].includes(item.key)).slice(0, 9);
|
||||||
|
const alertCount = activeAlerts?.total ?? 0;
|
||||||
|
const metricItems: WorkspaceQueueMetricRailItem[] = [
|
||||||
|
{
|
||||||
|
label: '速度',
|
||||||
|
value: <>{hasRealtimeSpeed(vehicle) ? formatNumber(vehicle.speedKmh, 1) : '—'}<em className="v2-monitor-detail-unit">{hasRealtimeSpeed(vehicle) ? 'km/h' : ''}</em></>,
|
||||||
|
note: '实时',
|
||||||
|
tone: 'primary',
|
||||||
|
emphasis: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '今日里程',
|
||||||
|
value: <>{dailyMileage == null ? '—' : formatNumber(dailyMileage, 1)}<em className="v2-monitor-detail-unit">{dailyMileage == null ? '' : 'km'}</em></>,
|
||||||
|
note: <span className="v2-monitor-detail-action-note">今日累计<IconChevronRight /></span>,
|
||||||
|
tone: 'primary',
|
||||||
|
emphasis: 'primary',
|
||||||
|
ariaLabel: '查看今日里程全部来源',
|
||||||
|
onClick: () => setSourceEvidenceOpen(true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '总里程',
|
||||||
|
value: <>{hasRealtimeMileage(vehicle) ? formatNumber(vehicle.totalMileageKm, 1) : '—'}<em className="v2-monitor-detail-unit">{hasRealtimeMileage(vehicle) ? 'km' : ''}</em></>,
|
||||||
|
note: <span className="v2-monitor-detail-action-note" title={mileageLabel}>{mileageLabel}<IconChevronRight /></span>,
|
||||||
|
emphasis: 'secondary',
|
||||||
|
ariaLabel: '查看总里程全部来源',
|
||||||
|
onClick: () => setSourceEvidenceOpen(true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SOC',
|
||||||
|
value: <>{hasRealtimeSOC(vehicle) ? formatNumber(vehicle.socPercent, 1) : '—'}<em className="v2-monitor-detail-unit">{hasRealtimeSOC(vehicle) ? '%' : ''}</em></>,
|
||||||
|
note: '剩余电量',
|
||||||
|
tone: 'success',
|
||||||
|
emphasis: 'secondary'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return <Card className="v2-vehicle-detail" bodyStyle={{ padding: 0 }}>
|
return <Card className="v2-vehicle-detail" bodyStyle={{ padding: 0 }}>
|
||||||
<div className="v2-detail-body" role="region" aria-label={`${vehicle.plate || vehicle.vin}车辆详情`}>
|
<div className="v2-detail-body" role="region" aria-label={`${vehicle.plate || vehicle.vin}车辆详情`}>
|
||||||
@@ -110,14 +145,22 @@ export default function MonitorVehicleDetailCard({
|
|||||||
description={accessSourceLabel}
|
description={accessSourceLabel}
|
||||||
meta={<ProtocolTag protocol={vehicle.primaryProtocol} compact unknownLabel="未知协议" />}
|
meta={<ProtocolTag protocol={vehicle.primaryProtocol} compact unknownLabel="未知协议" />}
|
||||||
/>
|
/>
|
||||||
<div className="v2-metric-grid">
|
<WorkspaceMetricRail
|
||||||
<div className="is-speed"><small>速度</small><strong>{hasRealtimeSpeed(vehicle) ? formatNumber(vehicle.speedKmh, 1) : '—'}<em>{hasRealtimeSpeed(vehicle) ? 'km/h' : ''}</em></strong></div>
|
variant="queue"
|
||||||
<div className="is-soc"><small>SOC</small><strong>{hasRealtimeSOC(vehicle) ? formatNumber(vehicle.socPercent, 1) : '—'}<em>{hasRealtimeSOC(vehicle) ? '%' : ''}</em></strong></div>
|
className="v2-monitor-detail-metric-rail"
|
||||||
<div className="is-mileage"><small>{mileageLabel}</small><Button theme="borderless" type="tertiary" className="v2-metric-source-link" aria-label="查看总里程全部来源" title="展开全部里程来源" onClick={() => setSourceEvidenceOpen(true)}><span>{hasRealtimeMileage(vehicle) ? formatNumber(vehicle.totalMileageKm, 1) : '—'}<em>{hasRealtimeMileage(vehicle) ? 'km' : ''}</em></span><IconChevronRight /></Button></div>
|
ariaLabel="车辆实时状态指标"
|
||||||
<div className="is-today"><small>今日里程</small><Button theme="borderless" type="tertiary" className="v2-metric-source-link" aria-label="查看今日里程全部来源" title="展开全部里程来源" onClick={() => setSourceEvidenceOpen(true)}><span>{dailyMileage == null ? '—' : formatNumber(dailyMileage, 1)}<em>{dailyMileage == null ? '' : 'km'}</em></span><IconChevronRight /></Button></div>
|
items={metricItems}
|
||||||
<div className="is-status"><small>状态</small><strong>{statusLabel(status)}</strong></div>
|
context={<div className="v2-monitor-detail-state">
|
||||||
<div className={`is-alert${(activeAlerts?.total ?? 0) > 0 ? ' has-alerts' : ''}`}><small>当前告警</small><strong>{formatNumber(activeAlerts?.total ?? 0)}<em>条</em></strong></div>
|
<span>
|
||||||
</div>
|
<small>车辆状态</small>
|
||||||
|
<Tag color={status === 'driving' ? 'blue' : status === 'idle' || status === 'online' ? 'green' : status === 'alert' ? 'red' : 'grey'} type="light" size="small">{statusLabel(status)}</Tag>
|
||||||
|
</span>
|
||||||
|
<span className={alertCount > 0 ? 'has-alerts' : ''}>
|
||||||
|
<small>当前告警</small>
|
||||||
|
<strong>{formatNumber(alertCount)}<em>条</em></strong>
|
||||||
|
</span>
|
||||||
|
</div>}
|
||||||
|
/>
|
||||||
{gbHighlights.length ? <div className="v2-gb-highlight-grid">{gbHighlights.map((item) => <div key={`${item.key}-${item.sourceField}`} title={item.description}><small>{item.label}</small><strong>{formatTelemetryValue(item.value, item.displayValue)}<em>{item.unit}</em></strong></div>)}</div> : null}
|
{gbHighlights.length ? <div className="v2-gb-highlight-grid">{gbHighlights.map((item) => <div key={`${item.key}-${item.sourceField}`} title={item.description}><small>{item.label}</small><strong>{formatTelemetryValue(item.value, item.displayValue)}<em>{item.unit}</em></strong></div>)}</div> : null}
|
||||||
</section>
|
</section>
|
||||||
<section className="v2-detail-section">
|
<section className="v2-detail-section">
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ describe('V2 production entry', () => {
|
|||||||
expect(vehicleSearchSource).toContain('v2-profile-sync-panel');
|
expect(vehicleSearchSource).toContain('v2-profile-sync-panel');
|
||||||
expect(corePageSources.MonitorPage).toContain("lazy(() => import('./MonitorVehicleDetailCard'))");
|
expect(corePageSources.MonitorPage).toContain("lazy(() => import('./MonitorVehicleDetailCard'))");
|
||||||
expect(monitorVehicleDetailSource).toContain('<Card className="v2-vehicle-detail"');
|
expect(monitorVehicleDetailSource).toContain('<Card className="v2-vehicle-detail"');
|
||||||
|
expect(monitorVehicleDetailSource).toContain("from '../shared/WorkspaceMetricRail'");
|
||||||
|
expect(monitorVehicleDetailSource).toContain('<WorkspaceMetricRail');
|
||||||
|
expect(monitorVehicleDetailSource).toContain('className="v2-monitor-detail-metric-rail"');
|
||||||
|
expect(monitorVehicleDetailSource).not.toContain('className="v2-metric-grid"');
|
||||||
expect(corePageSources.MonitorPage).toContain('<Card className="v2-detail-peek"');
|
expect(corePageSources.MonitorPage).toContain('<Card className="v2-detail-peek"');
|
||||||
expect(corePageSources.MonitorPage).toContain('<Card className="v2-event-strip"');
|
expect(corePageSources.MonitorPage).toContain('<Card className="v2-event-strip"');
|
||||||
expect(monitorVehicleDetailSource).toContain('<Tag className={`v2-status-text');
|
expect(monitorVehicleDetailSource).toContain('<Tag className={`v2-status-text');
|
||||||
|
|||||||
@@ -14095,32 +14095,6 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-detail-section > .v2-metric-grid {
|
|
||||||
overflow: hidden;
|
|
||||||
border-color: #dce5ef;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #fbfcfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v2-detail-section > .v2-metric-grid > div {
|
|
||||||
min-height: 70px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v2-detail-section > .v2-metric-grid > div:hover {
|
|
||||||
background: #fbfdff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-speed::after { background: #78a9ef; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-soc::after { background: #65bf94; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-mileage::after { background: #79a7ea; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-today::after { background: #2f7de9; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-status::after { background: #43b77d; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-alert::after { background: #d8e0ea; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-today strong,
|
|
||||||
.v2-detail-section > .v2-metric-grid > .is-today .v2-metric-source-link { color: #1268df; }
|
|
||||||
.v2-detail-section > .v2-metric-grid > .has-alerts::after { background: #eb6d70; }
|
|
||||||
|
|
||||||
.v2-vehicle-detail .v2-source-evidence.is-compact.semi-card {
|
.v2-vehicle-detail .v2-source-evidence.is-compact.semi-card {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -22949,3 +22923,170 @@
|
|||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global-monitor vehicle inspector.
|
||||||
|
* Reuse the shared Semi metric rail inside the narrow inspector, keeping the
|
||||||
|
* four decision metrics in a 2 × 2 matrix and moving state/alerts into one
|
||||||
|
* compact evidence footer.
|
||||||
|
*/
|
||||||
|
.v2-monitor-detail-metric-rail.is-queue.semi-card {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
margin: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dce5ef;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail.is-queue > .semi-card-body {
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-action.semi-button,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-action .semi-button-content {
|
||||||
|
min-height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-action .semi-button-content {
|
||||||
|
column-gap: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span.is-action {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span:nth-child(odd) {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span:nth-child(n + 3) {
|
||||||
|
border-top: 1px solid #e5ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span > strong,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-action .semi-button-content > strong,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span.is-secondary > strong,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span.is-secondary .v2-workspace-metric-action strong {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span > em,
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-action .semi-button-content > em {
|
||||||
|
max-width: 62px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-unit {
|
||||||
|
margin-left: 3px;
|
||||||
|
color: #8795a7;
|
||||||
|
font-size: 7px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 550;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span.is-primary .v2-monitor-detail-unit {
|
||||||
|
color: #7092c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-action-note {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-action-note > .semi-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-context {
|
||||||
|
display: flex;
|
||||||
|
min-height: 40px;
|
||||||
|
border-top: 1px solid #e5ebf2;
|
||||||
|
border-left: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state > span {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state > span + span {
|
||||||
|
border-left: 1px solid #e5ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state small {
|
||||||
|
color: #78879a;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 650;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state .semi-tag {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 70px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state strong {
|
||||||
|
color: #30465e;
|
||||||
|
font-size: 14px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state strong > em {
|
||||||
|
margin-left: 2px;
|
||||||
|
color: #8492a4;
|
||||||
|
font-size: 7px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-state > .has-alerts strong {
|
||||||
|
color: #c2413d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span.is-secondary {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span.is-secondary.is-action {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-detail-metric-rail .v2-workspace-metric-context {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user