diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx index 4bb0c6ef..748b23e2 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx @@ -83,12 +83,13 @@ test('polls lightweight single-vehicle realtime data and passes its report inter 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('速度'); - expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(2)')).toHaveTextContent('SOC'); - expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(4)')).toHaveTextContent('当日里程'); - expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(5)')).toHaveTextContent('推荐来源JT/T 808'); + expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-workspace-metric-list'); + expect(commandCard?.querySelector('.v2-vehicle-live-metric-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue'); + expect(commandCard?.querySelectorAll('.v2-workspace-metric-list > [role="listitem"]')).toHaveLength(5); + expect(commandCard?.querySelector('.v2-workspace-metric-list > span:first-child')).toHaveTextContent('速度'); + expect(commandCard?.querySelector('.v2-workspace-metric-list > span:nth-child(2)')).toHaveTextContent('SOC'); + expect(commandCard?.querySelector('.v2-workspace-metric-list > span:nth-child(4)')).toHaveTextContent('当日里程'); + expect(commandCard?.querySelector('.v2-vehicle-live-source-context')).toHaveTextContent('推荐来源JT/T 808'); expect(commandCard?.querySelector('.v2-live-primary-protocol')).toHaveClass('v2-protocol-tag', 'semi-tag-cyan-light'); expect(commandCard?.querySelector('.v2-identity-sources .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light'); expect(screen.getByLabelText('粤A12345 车辆档案')).toHaveFocus(); @@ -112,9 +113,9 @@ test('polls lightweight single-vehicle realtime data and passes its report inter expect(locationSource).toHaveClass('semi-button', 'v2-map-source-link'); expect(locationSource.closest('.semi-card')).toHaveClass('v2-single-map-card'); const totalMileageSource = screen.getByRole('button', { name: '查看总里程全部来源' }); - expect(totalMileageSource).toHaveClass('semi-button', 'v2-live-source-link'); - expect(totalMileageSource.closest('.semi-card')).toHaveClass('v2-live-overview'); - expect(totalMileageSource.closest('.semi-card')).toBe(commandCard); + expect(totalMileageSource).toHaveClass('semi-button', 'v2-workspace-metric-action'); + expect(totalMileageSource.closest('.semi-card')).toHaveClass('v2-vehicle-live-metric-rail'); + expect(totalMileageSource.closest('.v2-live-overview')).toBe(commandCard); fireEvent.click(locationSource); await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1)); }); @@ -445,7 +446,7 @@ test('shows unavailable live fields as dashes instead of fabricated zeroes', asy const liveOverview = (await screen.findByText('最新上报')).closest('.semi-card')!; for (const label of ['速度', 'SOC', '总里程', '当日里程']) { - const metric = Array.from(liveOverview.querySelectorAll('.v2-live-grid > div')).find((item) => item.querySelector('small')?.textContent === label); + const metric = Array.from(liveOverview.querySelectorAll('.v2-workspace-metric-list > [role="listitem"]')).find((item) => item.querySelector('small')?.textContent === label); expect(metric).toHaveTextContent('—'); expect(metric).not.toHaveTextContent('88.8'); expect(metric).not.toHaveTextContent(/^0/); @@ -562,11 +563,11 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => { expect(document.querySelector('.v2-identity-sources .semi-tag-blue-light')).toHaveTextContent('GB/T 32960'); expect(document.querySelector('.v2-identity-sources .semi-tag-cyan-light')).toHaveTextContent('JT/T 808'); expect(document.querySelector('.v2-identity-sources .semi-tag-violet-light')).toHaveTextContent('宇通 MQTT'); - expect(document.querySelector('.v2-vehicle-command-card .v2-live-grid')).toHaveAttribute('aria-label', '车辆实时指标'); - expect(document.querySelectorAll('.v2-vehicle-command-card .v2-live-grid > [role="listitem"]')).toHaveLength(6); - expect(document.querySelector('.v2-live-metric.is-speed')).toHaveTextContent('速度'); - expect(document.querySelector('.v2-live-metric.is-today-mileage')).toHaveTextContent('当日里程'); - expect(document.querySelector('.v2-live-metric.is-source-status')).toHaveTextContent('来源状态'); + expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list')).toHaveAttribute('aria-label', '车辆实时指标'); + expect(document.querySelectorAll('.v2-vehicle-command-card .v2-workspace-metric-list > [role="listitem"]')).toHaveLength(5); + expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list > span:first-child')).toHaveTextContent('速度'); + expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list > span:nth-child(4)')).toHaveTextContent('当日里程'); + expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list > span:nth-child(5)')).toHaveTextContent('来源状态'); fireEvent.click(screen.getByRole('button', { name: '历史数据' })); expect(await screen.findByTestId('mobile-history-route')).toBeInTheDocument(); }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index 726a3aa7..696c5e78 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -23,6 +23,7 @@ import { ProtocolTag } from '../shared/ProtocolTag'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; +import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet'; import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext'; @@ -466,6 +467,46 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele ...(hasMenu(session, 'statistics') ? [{ key: 'statistics', label: '里程查询', icon: , to: withMonitorReturn(`/statistics?vins=${encodeURIComponent(detail.vin)}`, monitorReturn), type: 'tertiary' as const }] : []), ...(hasMenu(session, 'alerts') ? [{ key: 'alerts', label: '告警事件', icon: , to: `/alerts?vin=${encodeURIComponent(detail.vin)}`, type: 'tertiary' as const }] : []) ]; + const liveMetrics: WorkspaceQueueMetricRailItem[] = [ + { + label: '速度', + value: <>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}km/h, + note: '实时车速', + tone: 'primary', + emphasis: 'primary' + }, + { + label: 'SOC', + value: <>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}%, + note: '剩余电量', + tone: 'success', + emphasis: 'secondary' + }, + { + label: '总里程', + value: <>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}km, + note: 推荐口径, + emphasis: 'secondary', + ariaLabel: '查看总里程全部来源', + onClick: () => setSourceEvidenceOpen(true) + }, + { + label: '当日里程', + value: <>{availableMetric(realtime?.todayMileageKm, realtime?.todayMileageAvailable)}km, + note: 今日累计, + tone: 'primary', + emphasis: 'primary', + ariaLabel: '查看当日里程全部来源', + onClick: () => setSourceEvidenceOpen(true) + }, + { + label: '来源状态', + value: <>{realtime?.onlineSourceCount ?? 0}/ {detail.sourceStatus.length}, + note: '在线来源 / 全部', + tone: (realtime?.onlineSourceCount ?? 0) > 0 ? 'success' : 'neutral', + emphasis: 'secondary' + } + ]; useLayoutEffect(() => { const root = recordRootRef.current; if (!root) return; @@ -529,14 +570,17 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele description="关键运行指标与推荐数据来源" meta={{realtime?.online ? '实时在线' : '当前离线'} · } /> -
-
速度{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}km/h实时车速
-
SOC{availableMetric(realtime?.socPercent, realtime?.socAvailable)}%剩余电量
-
总里程推荐口径
-
当日里程今日累计
-
推荐来源{protocolSourceLabel(realtime?.locationSource) || '当前最优来源'}
-
来源状态{realtime?.onlineSourceCount ?? 0} / {detail.sourceStatus.length}在线来源 / 全部
-
+ + 推荐来源 + + {protocolSourceLabel(realtime?.locationSource) || '当前最优来源'} + } + /> 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 f97cf367..5ee729e3 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -78,7 +78,10 @@ describe('V2 production entry', () => { expect(vehicleSearchSource).toContain(' { expect(workspaceStyles).toContain('grid-template-columns: repeat(2, minmax(0, 1fr));'); expect(workspaceStyles).toContain('.v2-vehicle-record-v3 .v2-vehicle-command-card > .semi-card-body {'); expect(workspaceStyles).toContain('grid-template-columns: minmax(210px, .62fr) minmax(0, 1.38fr);'); - expect(workspaceStyles).toContain('.v2-vehicle-record-v3 .v2-vehicle-command-card .v2-live-grid {'); + expect(workspaceStyles).toContain('.v2-vehicle-live-metric-rail.is-queue > .semi-card-body {'); + expect(workspaceStyles).toContain('.v2-vehicle-live-metric-rail .v2-workspace-metric-list {'); expect(workspaceStyles).toContain('.v2-vehicle-mobile-actions-sidesheet .v2-vehicle-mobile-actions-list {'); expect(workspaceStyles).toContain('.v2-access-table-v3 > .semi-card-body {'); expect(workspaceStyles).toContain('grid-template-columns: 112px minmax(0, 1fr);'); diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index 27869e0b..7dbb82e0 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -22782,3 +22782,170 @@ height: 46px; } } + +/* + * Single-vehicle live metrics use the shared Semi queue rail. + * The desktop rail keeps operating evidence in one scan line; mobile keeps + * the four driver-facing metrics in a compact 2 × 2 block so the map arrives + * sooner without removing mileage source evidence. + */ +.v2-vehicle-live-metric-rail.is-queue.semi-card { + border-bottom-color: #dfe7f0; +} + +.v2-vehicle-live-metric-rail.is-queue > .semi-card-body { + grid-template-columns: minmax(0, 1fr) 196px; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list { + grid-template-columns: .78fr .72fr 1.14fr 1.02fr .84fr; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span, +.v2-vehicle-live-metric-rail .v2-workspace-metric-action.semi-button, +.v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content { + min-height: 70px; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span, +.v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content { + padding: 8px 12px; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-action { + display: block; + padding: 0; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span > strong, +.v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content > strong { + overflow: hidden; + font-size: clamp(17px, 1.45vw, 21px); + text-overflow: ellipsis; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-secondary > strong, +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-secondary .v2-workspace-metric-action strong { + font-size: clamp(15px, 1.25vw, 18px); +} + +.v2-vehicle-live-unit { + margin-left: 4px; + color: #8492a4; + font-size: 9px; + font-style: normal; + font-weight: 550; + letter-spacing: 0; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-primary .v2-vehicle-live-unit { + color: #6f91bf; +} + +.v2-vehicle-live-action-note { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.v2-vehicle-live-action-note > .semi-icon { + flex: 0 0 auto; + font-size: 9px; +} + +.v2-vehicle-live-metric-rail .v2-workspace-metric-context { + padding: 8px 12px; +} + +.v2-vehicle-live-source-context { + align-items: center; +} + +.v2-vehicle-live-source-context > strong { + display: flex; + min-width: 0; + align-items: center; +} + +.v2-vehicle-record-v3 .v2-vehicle-command-card .v2-vehicle-live-source-context .v2-live-primary-protocol.semi-tag { + min-height: 23px; + max-width: 100%; + margin: 0; + border-radius: 7px; + padding-inline: 7px; + font-size: 9px; +} + +.v2-vehicle-live-source-context > em { + max-width: 172px; +} + +@media (max-width: 900px) { + .v2-vehicle-live-metric-rail.is-queue > .semi-card-body { + grid-template-columns: minmax(0, 1fr) 164px; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span, + .v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content { + padding-inline: 9px; + } +} + +@media (max-width: 680px) { + .v2-vehicle-live-metric-rail.is-queue > .semi-card-body { + min-height: 104px; + grid-template-columns: minmax(0, 1fr); + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-secondary { + display: grid; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-secondary.is-action { + display: block; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span:nth-child(n + 5) { + display: none; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span, + .v2-vehicle-live-metric-rail .v2-workspace-metric-action.semi-button, + .v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content { + min-height: 52px; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span:nth-child(odd) { + border-left: 0; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span:nth-child(n + 3) { + border-top: 1px solid #e5ebf2; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span, + .v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content { + padding: 5px 9px; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-action { + padding: 0; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span > strong, + .v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content > strong, + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-secondary > strong, + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span.is-secondary .v2-workspace-metric-action strong { + font-size: 16px; + line-height: 19px; + } + + .v2-vehicle-live-metric-rail .v2-workspace-metric-list > span > em, + .v2-vehicle-live-metric-rail .v2-workspace-metric-action .semi-button-content > em { + font-size: 7px; + } +}