From dcc6b223d985bae7e7d1492e3b79be27325a69aa Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 10:28:17 +0800 Subject: [PATCH] feat: unify monitor vehicle metric rail --- .../web/src/v2/pages/MonitorPage.test.tsx | 10 +- .../src/v2/pages/MonitorVehicleDetailCard.tsx | 59 +++++- .../apps/web/src/v2/productionEntry.test.ts | 4 + .../apps/web/src/v2/styles/workspace.css | 193 +++++++++++++++--- 4 files changed, 229 insertions(+), 37 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx index ba8eec88..cc002826 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx @@ -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); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorVehicleDetailCard.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorVehicleDetailCard.tsx index a2e6e87d..ac40aec8 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorVehicleDetailCard.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorVehicleDetailCard.tsx @@ -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) : '—'}{hasRealtimeSpeed(vehicle) ? 'km/h' : ''}, + note: '实时', + tone: 'primary', + emphasis: 'primary' + }, + { + label: '今日里程', + value: <>{dailyMileage == null ? '—' : formatNumber(dailyMileage, 1)}{dailyMileage == null ? '' : 'km'}, + note: 今日累计, + tone: 'primary', + emphasis: 'primary', + ariaLabel: '查看今日里程全部来源', + onClick: () => setSourceEvidenceOpen(true) + }, + { + label: '总里程', + value: <>{hasRealtimeMileage(vehicle) ? formatNumber(vehicle.totalMileageKm, 1) : '—'}{hasRealtimeMileage(vehicle) ? 'km' : ''}, + note: {mileageLabel}, + emphasis: 'secondary', + ariaLabel: '查看总里程全部来源', + onClick: () => setSourceEvidenceOpen(true) + }, + { + label: 'SOC', + value: <>{hasRealtimeSOC(vehicle) ? formatNumber(vehicle.socPercent, 1) : '—'}{hasRealtimeSOC(vehicle) ? '%' : ''}, + note: '剩余电量', + tone: 'success', + emphasis: 'secondary' + } + ]; return
@@ -110,14 +145,22 @@ export default function MonitorVehicleDetailCard({ description={accessSourceLabel} meta={} /> -
-
速度{hasRealtimeSpeed(vehicle) ? formatNumber(vehicle.speedKmh, 1) : '—'}{hasRealtimeSpeed(vehicle) ? 'km/h' : ''}
-
SOC{hasRealtimeSOC(vehicle) ? formatNumber(vehicle.socPercent, 1) : '—'}{hasRealtimeSOC(vehicle) ? '%' : ''}
-
{mileageLabel}
-
今日里程
-
状态{statusLabel(status)}
-
0 ? ' has-alerts' : ''}`}>当前告警{formatNumber(activeAlerts?.total ?? 0)}
-
+ + + 车辆状态 + {statusLabel(status)} + + 0 ? 'has-alerts' : ''}> + 当前告警 + {formatNumber(alertCount)} + +
} + /> {gbHighlights.length ?
{gbHighlights.map((item) =>
{item.label}{formatTelemetryValue(item.value, item.displayValue)}{item.unit}
)}
: null}
diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index 5ee729e3..47efe9cd 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -122,6 +122,10 @@ describe('V2 production entry', () => { expect(vehicleSearchSource).toContain('v2-profile-sync-panel'); expect(corePageSources.MonitorPage).toContain("lazy(() => import('./MonitorVehicleDetailCard'))"); expect(monitorVehicleDetailSource).toContain(' .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 { margin-top: 8px; overflow: hidden; @@ -22949,3 +22923,170 @@ 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; + } +}