feat: unify monitor vehicle metric rail

This commit is contained in:
lingniu
2026-07-19 10:28:17 +08:00
parent add47b7efc
commit dcc6b223d9
4 changed files with 229 additions and 37 deletions

View File

@@ -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-metric-grid > div:first-child')).toHaveTextContent('速度42');
expect(view.container.querySelector('.v2-metric-grid > div:nth-child(4)')).toHaveTextContent('今日里程18.6km');
expect(screen.getByRole('list', { name: '车辆实时状态指标' })).toHaveClass('v2-workspace-metric-list');
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(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.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', 'LTEST000000000001');
expect(vehicleCardArgsSpy).toHaveBeenLastCalledWith('LTEST000000000001', vehicles[0], true);

View File

@@ -10,6 +10,7 @@ import { useMonitorVehicleCard } from '../hooks/useMonitorData';
import { withMonitorReturn } from '../routing/monitorContext';
import { ProtocolTag } from '../shared/ProtocolTag';
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
function hasRealtimeLocation(vehicle: VehicleRealtimeRow) {
@@ -70,6 +71,40 @@ export default function MonitorVehicleDetailCard({
'hydrogen_concentration_percent', 'hydrogen_pressure_mpa', 'hydrogen_temperature_c',
'engine_speed_rpm', 'total_voltage_v', 'total_current_a'
].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 }}>
<div className="v2-detail-body" role="region" aria-label={`${vehicle.plate || vehicle.vin}车辆详情`}>
@@ -110,14 +145,22 @@ export default function MonitorVehicleDetailCard({
description={accessSourceLabel}
meta={<ProtocolTag protocol={vehicle.primaryProtocol} compact unknownLabel="未知协议" />}
/>
<div className="v2-metric-grid">
<div className="is-speed"><small></small><strong>{hasRealtimeSpeed(vehicle) ? formatNumber(vehicle.speedKmh, 1) : '—'}<em>{hasRealtimeSpeed(vehicle) ? 'km/h' : ''}</em></strong></div>
<div className="is-soc"><small>SOC</small><strong>{hasRealtimeSOC(vehicle) ? formatNumber(vehicle.socPercent, 1) : '—'}<em>{hasRealtimeSOC(vehicle) ? '%' : ''}</em></strong></div>
<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>
<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>
<div className="is-status"><small></small><strong>{statusLabel(status)}</strong></div>
<div className={`is-alert${(activeAlerts?.total ?? 0) > 0 ? ' has-alerts' : ''}`}><small></small><strong>{formatNumber(activeAlerts?.total ?? 0)}<em></em></strong></div>
</div>
<WorkspaceMetricRail
variant="queue"
className="v2-monitor-detail-metric-rail"
ariaLabel="车辆实时状态指标"
items={metricItems}
context={<div className="v2-monitor-detail-state">
<span>
<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}
</section>
<section className="v2-detail-section">