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 a5357144..001efe68 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
@@ -77,7 +77,10 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
await waitFor(() => expect(fleetMapVehicles).toHaveBeenLastCalledWith([
expect.objectContaining({ longitude: 113.28, latitude: 23.15, reportIntervalMs: 30_000 })
]));
- expect(view.container.querySelector('.v2-identity-band')).toHaveClass('semi-card');
+ const commandCard = view.container.querySelector('.v2-identity-band');
+ expect(commandCard).toHaveClass('semi-card', 'v2-vehicle-command-card', 'v2-live-overview');
+ expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-live-grid');
+ expect(commandCard?.querySelectorAll('.v2-live-grid > [role="listitem"]')).toHaveLength(6);
expect(screen.getByRole('heading', { name: '最新上报', level: 5 })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '实时位置', level: 5 })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '最近事件', level: 5 })).toBeInTheDocument();
@@ -99,6 +102,7 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
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);
fireEvent.click(locationSource);
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
});
@@ -347,4 +351,5 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => {
expect(document.querySelector('.v2-telemetry-table')).not.toBeInTheDocument();
expect(document.querySelector('.v2-telemetry-mobile-list.semi-list')).toBeInTheDocument();
expect(document.querySelector('.v2-telemetry-mobile-item.semi-list-item')).toHaveTextContent('36km/h正常');
+ expect(document.querySelector('.v2-vehicle-command-card .v2-live-grid')).toHaveAttribute('aria-label', '车辆实时指标');
});
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 5f458f38..1cd0b8b8 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
@@ -51,6 +51,12 @@ function vehicleLastSeenLabel(value?: string) {
return normalized.length >= 16 ? `${normalized.slice(5, 16)} 更新` : normalized;
}
+function eventTimeLabel(value?: string) {
+ if (!value) return '—';
+ const normalized = value.replace('T', ' ');
+ return normalized.length >= 16 ? normalized.slice(5, 16) : normalized;
+}
+
function VehicleSearch() {
const navigate = useNavigate();
const { session } = usePlatformSession();
@@ -255,7 +261,7 @@ function Events({ detail }: { detail: VehicleDetail }) {
emptyContent={}
renderItem={(event, index) =>
{event.tone === 'success' ? : }
- {event.title}{event.detail}
+ {event.title}{event.detail}
}
/>
;
@@ -409,16 +415,19 @@ function VehicleRecordNavigation() {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
target.focus({ preventScroll: true });
};
+ const navigationItems = [
+ ['location', '实时位置', ],
+ ['events', '最近事件', ],
+ ['telemetry', '实时遥测', ],
+ ['archive', '车辆主档', ]
+ ] as const;
return
- } aria-label="跳转到实时位置" onClick={() => jumpToSection('location')}>实时位置
- } aria-label="跳转到最近事件" onClick={() => jumpToSection('events')}>最近事件
- } aria-label="跳转到实时遥测" onClick={() => jumpToSection('telemetry')}>实时遥测
- } aria-label="跳转到车辆主档" onClick={() => jumpToSection('archive')}>车辆主档
+ {navigationItems.map(([key, label, icon]) => )}
}
/>
;
@@ -441,28 +450,37 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
];
return
-
- {fmt(identity?.plate || realtime?.plate)}{realtime?.online ? '在线' : '离线'}VIN{detail.vin}} onClick={() => navigator.clipboard?.writeText(detail.vin)} />
- 车辆品牌 / 车型{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}
可用数据来源{detail.sources.length ? detail.sources.map((source) => {source}) : 暂无来源}
-
- {actions.map((action) =>
)}
+
+
+
+ {fmt(identity?.plate || realtime?.plate)}
+ {realtime?.online ? '在线' : '离线'}
+ VIN{detail.vin}} onClick={() => navigator.clipboard?.writeText(detail.vin)} />
+
+
+
车辆品牌 / 车型{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}
+
可用数据来源{detail.sources.length ? detail.sources.map((source) => {source}) : 暂无来源}
+
+
+ {actions.map((action) => )}
+
-
-
-
- {realtime?.online ? '实时在线' : '当前离线'} · {fmt(realtime?.lastSeen || identity?.lastSeen)}}
- />
-
-
速度{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}km/h
-
SOC{availableMetric(realtime?.socPercent, realtime?.socAvailable)}%
-
总里程
-
当日里程
-
推荐来源{fmt(realtime?.primaryProtocol)}{realtime?.locationSource ? ` · ${realtime.locationSource}` : ''}
-
来源状态{realtime?.onlineSourceCount ?? 0} 在线 / {detail.sourceStatus.length} 个
-
-
+
+ 最新上报}
+ description="关键运行指标与推荐数据来源"
+ meta={{realtime?.online ? '实时在线' : '当前离线'} · {fmt(realtime?.lastSeen || identity?.lastSeen)}}
+ />
+
+
速度{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}km/h实时车速
+
SOC{availableMetric(realtime?.socPercent, realtime?.socAvailable)}%剩余电量
+
总里程推荐口径
+
当日里程今日累计
+
推荐来源{fmt(realtime?.primaryProtocol)}{realtime?.locationSource || '当前最优来源'}
+
来源状态{realtime?.onlineSourceCount ?? 0} / {detail.sourceStatus.length}在线来源 / 全部
+
+
+
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 d324114f..f6ca2636 100644
--- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
+++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
@@ -62,10 +62,11 @@ describe('V2 production entry', () => {
expect(corePageSources.StatisticsPage).not.toContain("from '../shared/PageHeader'");
expect(corePageSources.StatisticsPage).not.toContain('
.semi-card-body {
+ display: block;
+ min-height: 0;
+}
+
+.v2-vehicle-command-header {
+ display: grid;
+ grid-template-columns: minmax(310px, .9fr) minmax(360px, 1.15fr) minmax(360px, auto);
+ align-items: stretch;
+ background: #fff;
+}
+
+.v2-vehicle-command-header .v2-identity-primary {
+ display: grid;
+ grid-template-columns: auto auto;
+ grid-template-rows: auto auto;
+ align-content: center;
+ align-items: center;
+ justify-content: start;
+ gap: 8px 10px;
+ padding: 18px 20px;
+}
+
+.v2-vehicle-command-header .v2-plate {
+ min-height: 42px;
+ border-color: #b9d2fb;
+ border-radius: 9px;
+ background: #edf4ff;
+ padding-inline: 14px;
+ color: var(--v2-blue);
+ font-size: 18px;
+ letter-spacing: .01em;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .65);
+}
+
+.v2-vehicle-command-header .v2-online-label.semi-tag {
+ min-height: 25px;
+ margin: 0;
+ border-radius: 999px;
+ padding-inline: 9px;
+ font-size: 10px;
+ font-weight: 700;
+}
+
+.v2-identity-vin {
+ display: flex;
+ min-width: 0;
+ grid-column: 1 / -1;
+ align-items: center;
+ gap: 7px;
+}
+
+.v2-vehicle-command-header .v2-identity-vin small,
+.v2-vehicle-command-header .v2-identity-vin b,
+.v2-vehicle-command-header .v2-identity-vin .semi-button {
+ grid-row: auto;
+}
+
+.v2-vehicle-command-header .v2-identity-vin small {
+ flex: 0 0 auto;
+ color: #8290a2;
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.v2-vehicle-command-header .v2-identity-vin b {
+ min-width: 0;
+ overflow: hidden;
+ color: #41536a;
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: .015em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-vehicle-command-header .v2-identity-vin .semi-button {
+ width: 28px;
+ min-width: 28px;
+ height: 28px;
+ flex: 0 0 auto;
+ border-radius: 7px;
+ color: #73849a;
+}
+
+.v2-vehicle-command-header .v2-identity-meta {
+ display: grid;
+ min-width: 0;
+ grid-template-columns: minmax(180px, 1.15fr) minmax(160px, .85fr);
+ align-items: center;
+ gap: 0;
+ border: 0;
+ border-left: 1px solid #e4eaf2;
+ padding: 16px 0;
+}
+
+.v2-vehicle-command-header .v2-identity-meta > div {
+ min-width: 0;
+ padding: 0 20px;
+}
+
+.v2-vehicle-command-header .v2-identity-meta > div + div {
+ margin: 0;
+ border: 0;
+ border-left: 1px solid #e7edf4;
+ padding: 0 20px;
+}
+
+.v2-vehicle-command-header .v2-identity-meta small {
+ display: block;
+ color: #7b899b;
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.v2-vehicle-command-header .v2-identity-meta strong {
+ display: block;
+ margin-top: 7px;
+ overflow: hidden;
+ color: #2c4057;
+ font-size: 12px;
+ line-height: 1.45;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-vehicle-command-header .v2-identity-sources {
+ display: flex;
+ min-width: 0;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin: 7px 0 0;
+}
+
+.v2-vehicle-command-header .v2-identity-meta .semi-tag {
+ min-height: 22px;
+ margin: 0;
+ border-radius: 6px;
+ padding-inline: 7px;
+ font-size: 9px;
+ font-weight: 650;
+}
+
+.v2-vehicle-command-header .v2-identity-actions {
+ display: flex;
+ min-width: 0;
+ flex-wrap: wrap;
+ align-content: center;
+ justify-content: flex-end;
+ gap: 7px;
+ border-left: 1px solid #e4eaf2;
+ padding: 15px 18px;
+}
+
+.v2-vehicle-command-header .v2-identity-actions > .semi-button {
+ min-width: 100px;
+ min-height: 38px;
+ flex: 0 1 auto;
+ border-radius: 8px;
+ padding-inline: 12px;
+ font-size: 11px;
+ font-weight: 650;
+}
+
+.v2-vehicle-command-header .v2-identity-actions > .semi-button-primary {
+ border-color: #bcd3f8;
+ background: #edf4ff;
+ color: var(--v2-blue);
+}
+
+.v2-vehicle-live-section {
+ border-top: 1px solid #dfe7f0;
+ background: #fbfcfe;
+}
+
+.v2-vehicle-live-section > .v2-workspace-panel-header {
+ min-height: 58px;
+ border-bottom: 1px solid #e6ecf3;
+ background: #f8fafd;
+ padding: 10px 16px;
+}
+
+.v2-vehicle-live-section .v2-live-report-meta {
+ max-width: min(520px, 42vw);
+ color: #65778d;
+ font-size: 10px;
+ font-variant-numeric: tabular-nums;
+}
+
+.v2-vehicle-command-card .v2-live-grid {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ background: #fff;
+}
+
+.v2-vehicle-command-card .v2-live-grid > div {
+ position: relative;
+ min-width: 0;
+ min-height: 104px;
+ border-top: 0;
+ border-left: 1px solid #e7edf4;
+ padding: 15px 16px 14px;
+}
+
+.v2-vehicle-command-card .v2-live-grid > div:first-child {
+ border-left: 0;
+}
+
+.v2-vehicle-command-card .v2-live-grid > div:nth-child(4) {
+ border-left: 1px solid #e7edf4;
+}
+
+.v2-vehicle-command-card .v2-live-grid > div:nth-child(n + 4) {
+ border-top: 0;
+}
+
+.v2-vehicle-command-card .v2-live-grid > div::after {
+ position: absolute;
+ right: 16px;
+ bottom: 0;
+ left: 16px;
+ height: 3px;
+ border-radius: 999px 999px 0 0;
+ background: #dce5ef;
+ content: "";
+}
+
+.v2-vehicle-command-card .v2-live-grid > div:nth-child(-n + 4)::after {
+ background: #8eb6f3;
+}
+
+.v2-vehicle-command-card .v2-live-grid small {
+ color: #6c7d92;
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.v2-vehicle-command-card .v2-live-grid strong,
+.v2-vehicle-command-card .v2-live-source-link {
+ margin-top: 9px;
+ color: #213950;
+ font-size: 22px;
+ font-weight: 750;
+ letter-spacing: -.025em;
+ line-height: 1;
+}
+
+.v2-vehicle-command-card .v2-live-grid strong.is-text {
+ font-size: 15px;
+ letter-spacing: 0;
+}
+
+.v2-vehicle-command-card .v2-live-source-link .semi-button-content {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 6px;
+}
+
+.v2-vehicle-command-card .v2-live-grid em {
+ margin-left: 4px;
+ color: #8290a3;
+ font-size: 9px;
+ letter-spacing: 0;
+}
+
+.v2-vehicle-command-card .v2-live-grid > div > span:last-child {
+ display: block;
+ margin-top: 8px;
+ overflow: hidden;
+ color: #8996a7;
+ font-size: 9px;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-vehicle-command-card .v2-current-location {
+ min-height: 58px;
+ grid-template-columns: minmax(310px, .8fr) minmax(420px, 1.2fr);
+ border-top: 1px solid #e4eaf2;
+ background: #f8fafd;
+}
+
+.v2-vehicle-command-card .v2-current-location > span {
+ padding: 9px 16px;
+}
+
+.v2-vehicle-command-card .v2-current-location small {
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.v2-vehicle-command-card .v2-current-location strong {
+ color: #405269;
+ font-size: 11px;
+}
+
+.v2-vehicle-command-card .v2-current-address-action.semi-button {
+ min-height: 30px;
+ border-radius: 7px;
+ font-size: 10px;
+}
+
+.v2-vehicle-record-page .v2-record-grid {
+ gap: 14px;
+}
+
+.v2-vehicle-record-page .v2-events-card .v2-event-list .v2-event-row {
+ min-height: 48px;
+}
+
+.v2-vehicle-record-page .v2-events-card .v2-event-row strong {
+ font-size: 11px;
+}
+
+.v2-vehicle-record-page .v2-events-card .v2-event-row p,
+.v2-vehicle-record-page .v2-events-card .v2-event-row time {
+ font-size: 9px;
+}
+
+@media (max-width: 1400px) {
+ .v2-vehicle-command-header {
+ grid-template-columns: minmax(300px, .85fr) minmax(0, 1.15fr);
+ }
+
+ .v2-vehicle-command-header .v2-identity-actions {
+ grid-column: 1 / -1;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ overflow-x: auto;
+ border-top: 1px solid #e4eaf2;
+ border-left: 0;
+ padding: 9px 14px;
+ scrollbar-width: thin;
+ }
+
+ .v2-vehicle-command-header .v2-identity-actions > .semi-button {
+ min-width: 118px;
+ flex: 0 0 auto;
+ }
+}
+
+@media (max-width: 900px) {
+ .v2-vehicle-record-page {
+ gap: 10px;
+ padding: 10px 12px 14px;
+ }
+
+ .v2-vehicle-command-header {
+ grid-template-columns: minmax(250px, .8fr) minmax(0, 1.2fr);
+ }
+
+ .v2-vehicle-command-header .v2-identity-primary {
+ padding: 14px 16px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta {
+ grid-template-columns: minmax(0, 1.1fr) minmax(130px, .9fr);
+ border-top: 0;
+ border-bottom: 0;
+ border-left: 1px solid #e4eaf2;
+ padding: 13px 0;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta > div {
+ padding-inline: 15px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta > div + div {
+ margin: 0;
+ border-top: 0;
+ border-left: 1px solid #e7edf4;
+ padding: 0 15px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-actions {
+ display: flex;
+ grid-template-columns: none;
+ gap: 7px;
+ }
+
+ .v2-vehicle-command-card .v2-live-grid {
+ display: flex;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ scroll-snap-type: x proximity;
+ scrollbar-width: thin;
+ }
+
+ .v2-vehicle-command-card .v2-live-grid > div {
+ min-width: 148px;
+ min-height: 100px;
+ flex: 0 0 148px;
+ border-top: 0;
+ border-left: 1px solid #e7edf4;
+ scroll-snap-align: start;
+ }
+
+ .v2-vehicle-command-card .v2-live-grid > div:first-child {
+ border-left: 0;
+ }
+
+ .v2-vehicle-command-card .v2-current-location {
+ grid-template-columns: minmax(210px, .78fr) minmax(0, 1.22fr);
+ }
+
+ .v2-vehicle-command-card .v2-current-location > span + span {
+ border-top: 0;
+ border-left: 1px solid #e4eaf2;
+ }
+
+ .v2-vehicle-record-nav .v2-workspace-panel-header {
+ min-height: 50px;
+ padding: 7px 9px;
+ }
+
+ .v2-vehicle-record-nav .v2-workspace-panel-copy > .semi-typography {
+ display: none;
+ }
+}
+
+@media (max-width: 680px) {
+ .v2-vehicle-record-page {
+ gap: 8px;
+ padding: 8px;
+ }
+
+ .v2-vehicle-command-card.semi-card {
+ border-radius: 11px;
+ }
+
+ .v2-vehicle-command-header {
+ grid-template-columns: 1fr;
+ }
+
+ .v2-vehicle-command-header .v2-identity-primary {
+ padding: 13px 13px 11px;
+ }
+
+ .v2-vehicle-command-header .v2-plate {
+ min-height: 38px;
+ padding-inline: 12px;
+ font-size: 16px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta {
+ grid-template-columns: minmax(0, 1.15fr) minmax(120px, .85fr);
+ border-top: 1px solid #e7edf4;
+ border-left: 0;
+ padding: 10px 0;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta > div {
+ padding-inline: 12px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta > div + div {
+ padding-inline: 12px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta strong {
+ margin-top: 5px;
+ font-size: 11px;
+ }
+
+ .v2-vehicle-command-header .v2-identity-sources {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ margin-top: 5px;
+ scrollbar-width: none;
+ }
+
+ .v2-vehicle-command-header .v2-identity-meta .semi-tag {
+ flex: 0 0 auto;
+ }
+
+ .v2-vehicle-command-header .v2-identity-actions {
+ display: flex;
+ grid-template-columns: none;
+ gap: 6px;
+ padding: 7px 10px;
+ scrollbar-width: none;
+ }
+
+ .v2-vehicle-command-header .v2-identity-actions::-webkit-scrollbar,
+ .v2-vehicle-command-header .v2-identity-sources::-webkit-scrollbar {
+ display: none;
+ }
+
+ .v2-vehicle-command-header .v2-identity-actions > .semi-button {
+ width: auto;
+ min-width: 112px;
+ min-height: 38px;
+ flex: 0 0 auto;
+ }
+
+ .v2-vehicle-live-section > .v2-workspace-panel-header {
+ min-height: 52px;
+ padding: 8px 11px;
+ }
+
+ .v2-vehicle-live-section .v2-workspace-panel-copy > .semi-typography {
+ display: none;
+ }
+
+ .v2-vehicle-live-section .v2-live-report-meta {
+ max-width: 48vw;
+ font-size: 9px;
+ }
+
+ .v2-vehicle-command-card .v2-live-grid > div {
+ min-width: 138px;
+ min-height: 94px;
+ flex-basis: 138px;
+ padding: 13px 12px 12px;
+ }
+
+ .v2-vehicle-command-card .v2-live-grid strong,
+ .v2-vehicle-command-card .v2-live-source-link {
+ margin-top: 8px;
+ font-size: 20px;
+ }
+
+ .v2-vehicle-command-card .v2-current-location {
+ grid-template-columns: 1fr;
+ }
+
+ .v2-vehicle-command-card .v2-current-location > span {
+ min-height: 46px;
+ padding: 8px 11px;
+ }
+
+ .v2-vehicle-command-card .v2-current-location > span + span {
+ border-top: 1px solid #e4eaf2;
+ border-left: 0;
+ }
+
+ .v2-vehicle-record-nav .v2-workspace-panel-header {
+ grid-template-columns: auto minmax(0, 1fr);
+ }
+
+ .v2-vehicle-record-nav .v2-workspace-panel-copy > h5.semi-typography {
+ font-size: 11px;
+ }
+
+ .v2-vehicle-section-nav > .semi-button {
+ min-height: 32px;
+ }
+
+ .v2-vehicle-record-page .v2-record-grid {
+ gap: 10px;
+ }
+
+ .v2-vehicle-record-page .v2-single-map-card {
+ min-height: 330px;
+ }
+}