From c29d41fd86a599f956234116f010188c840ec2f2 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 02:24:21 +0800 Subject: [PATCH] refine Semi UI vehicle detail workspace --- .../web/src/v2/pages/VehiclePage.test.tsx | 7 +- .../apps/web/src/v2/pages/VehiclePage.tsx | 74 ++- .../apps/web/src/v2/productionEntry.test.ts | 5 +- .../apps/web/src/v2/styles/workspace.css | 571 ++++++++++++++++++ 4 files changed, 626 insertions(+), 31 deletions(-) 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 - - - - + {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}
-
车辆品牌 / 车型{[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}
+
+
车辆品牌 / 车型{[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; + } +}