From 32d97a0ddca5f34f942265a5d513fd531bfd95b5 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 05:40:47 +0800 Subject: [PATCH] refine Semi UI monitor vehicle inspector --- .../web/src/v2/pages/MonitorPage.test.tsx | 7 + .../apps/web/src/v2/pages/MonitorPage.tsx | 46 ++- .../apps/web/src/v2/styles/workspace.css | 372 ++++++++++++++++++ 3 files changed, 408 insertions(+), 17 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 f982fb69..f06d8946 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 @@ -128,6 +128,12 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(kpis?.querySelector('.v2-kpi:last-child')).toHaveClass('is-missing', 'is-support'); expect(view.container.querySelector('.v2-event-strip')).toHaveClass('semi-card'); expect(view.container.querySelector('.v2-monitor-live-tag')).toHaveClass('semi-tag'); + const liveStatus = screen.getByLabelText('实时数据状态'); + expect(liveStatus).toHaveTextContent('数据已同步'); + expect(liveStatus).toHaveTextContent('实时数据2 辆已载入'); + expect(liveStatus).toHaveTextContent('地图载荷0 个车辆点'); + expect(liveStatus).toHaveTextContent('智能刷新重点 10s · 车队 15s · 统计 30s'); + expect(liveStatus).toHaveTextContent('最近同步等待数据'); expect(workspace).not.toHaveClass('is-detail-open'); expect(workspace).not.toHaveClass('is-detail-collapsed'); expect(firstVehicle).not.toHaveClass('is-selected'); @@ -151,6 +157,7 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(screen.getByRole('heading', { name, level: 5 })).toBeInTheDocument(); } expect(screen.getByText('身份、车型与来源覆盖')).toBeInTheDocument(); + expect(screen.getByText('1/1 在线').closest('.semi-tag')).toBeInTheDocument(); expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('平台接收时间'); expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('主协议JT808'); expect(view.container.querySelector('.v2-metric-grid > div:first-child')).toHaveTextContent('速度42'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index e96ddbf0..797a2c36 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -266,19 +266,20 @@ function VehicleDetailCard({ {vehicle.vin}
{relativeFreshness(vehicle.lastSeen)}} />
- 平台接收时间{vehicle.lastSeen || '暂无上报'} - 主协议{vehicle.primaryProtocol || '未知'} - 接入来源{accessSource || '待补充'} + 平台接收时间{vehicle.lastSeen || '暂无上报'} + 主协议{vehicle.primaryProtocol || '未知'} + 接入来源{accessSource || '待补充'}
坐标
@@ -293,12 +294,12 @@ function VehicleDetailCard({ meta={{vehicle.primaryProtocol || '未知协议'}} />
-
速度{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)}
+
速度{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)}
{gbHighlights.length ?
{gbHighlights.map((item) =>
{item.label}{formatTelemetryValue(item.value, item.displayValue)}{item.unit}
)}
: null}
@@ -307,7 +308,7 @@ function VehicleDetailCard({ variant="compact" title="车辆信息" description="身份、车型与来源覆盖" - meta={`${vehicle.onlineSourceCount}/${vehicle.sourceCount} 在线`} + meta={{vehicle.onlineSourceCount}/{vehicle.sourceCount} 在线} />
VIN
{vehicle.vin}
@@ -426,6 +427,12 @@ export default function MonitorPage() { const driving = rows.filter((vehicle) => vehicleStatus(vehicle) === 'driving').length; const idle = rows.filter((vehicle) => vehicleStatus(vehicle) === 'idle').length; const offline = rows.filter((vehicle) => vehicleStatus(vehicle) === 'offline').length; + const viewportLoad = mode === 'map' + ? (map.data?.clusters.length + ? `${map.data.clusters.length} 个聚合 · ${map.data.points.length} 个车辆点` + : `${map.data?.points.length ?? 0} 个车辆点`) + : `${realtimeListQuery.data?.items.length ?? 0} / ${realtimeListQuery.data?.total ?? 0} 辆`; + const lastSyncAt = vehicles.dataUpdatedAt ? new Date(vehicles.dataUpdatedAt) : undefined; return (
@@ -533,12 +540,17 @@ export default function MonitorPage() { ) : null} : setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />} - - 实时数据状态 - {vehicles.isFetching ? '正在刷新' : '实时车辆已同步'} - {mode === 'map' ? `列表 ${visibleRows.length} 条 · 地图 ${map.data?.clusters.length ? `${map.data.clusters.length} 个聚合 + ${map.data.points.length} 个车辆点` : `${map.data?.points.length ?? 0} 个点`}` : `实时列表 ${realtimeListQuery.data?.items.length ?? 0} / ${realtimeListQuery.data?.total ?? 0} 辆`} - 智能刷新重点车辆 {MONITOR_REFRESH.selected / 1000} 秒 · 车队 {MONITOR_REFRESH.fleet / 1000} 秒 · 统计 {MONITOR_REFRESH.summary / 1000} 秒 - + +
+ + 实时数据{visibleRows.length.toLocaleString('zh-CN')} 辆已载入 +
+
{mode === 'map' ? '地图载荷' : '列表载荷'}{viewportLoad}
+
+ + 重点 {MONITOR_REFRESH.selected / 1000}s · 车队 {MONITOR_REFRESH.fleet / 1000}s · 统计 {MONITOR_REFRESH.summary / 1000}s +
+
{mobileEntryOpen ? setMobileEntryOpen(false)} /> : null}
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 69337b3d..bebf8f81 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -9192,3 +9192,375 @@ margin-top: 7px; } } + +/* Semi UI workspace migration: monitor vehicle inspector and live status rail. */ +.v2-vehicle-detail.semi-card { + background: #f6f8fb; + padding: 12px; + scrollbar-color: #a8b7c9 transparent; + scrollbar-width: thin; +} + +.v2-vehicle-detail.semi-card::-webkit-scrollbar { + width: 8px; +} + +.v2-vehicle-detail.semi-card::-webkit-scrollbar-thumb { + border: 2px solid transparent; + border-radius: 999px; + background: #a8b7c9; + background-clip: padding-box; +} + +.v2-detail-shell-header { + top: -12px; + margin: -12px -12px 8px; + border-bottom-color: #dfe7f0; + background: rgba(255, 255, 255, .96); + padding: 13px 12px 10px; + box-shadow: 0 6px 18px rgba(31, 53, 80, .045); + backdrop-filter: blur(10px); +} + +.v2-detail-shell-header .v2-detail-controls { + top: 12px; + right: 12px; +} + +.v2-detail-shell-header .v2-detail-title { + padding-bottom: 11px; +} + +.v2-detail-shell-header .v2-detail-title strong { + font-size: 19px; + font-weight: 780; + letter-spacing: -.035em; +} + +.v2-detail-actions { + gap: 5px; +} + +.v2-detail-actions a { + justify-content: space-between; + padding: 0 9px; + border-color: #dce5ef; + background: #f9fbfd; +} + +.v2-detail-actions a > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-detail-actions a > .semi-icon { + color: #94a3b8; + font-size: 11px; +} + +.v2-detail-actions a:first-child > .semi-icon, +.v2-detail-actions a:hover > .semi-icon, +.v2-detail-actions a:focus-visible > .semi-icon { + color: currentColor; +} + +.v2-vehicle-detail .v2-detail-section { + border-color: #dce5ef; + border-radius: 11px; + box-shadow: 0 6px 20px rgba(31, 53, 80, .045); +} + +.v2-detail-section > .v2-workspace-panel-header { + min-height: 50px; + padding: 9px 11px; +} + +.v2-detail-section > .v2-workspace-panel-header .v2-workspace-panel-copy { + gap: 3px; +} + +.v2-detail-section > .v2-workspace-panel-header .v2-workspace-panel-copy > h5 { + color: #263a52; + font-size: 12px; + font-weight: 730; +} + +.v2-detail-section > .v2-workspace-panel-header .v2-workspace-panel-copy > .semi-typography { + color: #8794a6; +} + +.v2-vehicle-detail .v2-detail-report { + border-color: #cfdff3; +} + +.v2-detail-report > .v2-workspace-panel-header { + background: linear-gradient(100deg, #f1f7ff 0%, #f8fbff 64%, #fff 100%); +} + +.v2-report-summary { + border-color: #d5e2f1; + background: #fafcff; +} + +.v2-report-summary > .is-time { + background: linear-gradient(135deg, #f2f7ff 0%, #fafcff 100%); +} + +.v2-report-summary strong { + color: #174d8d; + font-weight: 750; +} + +.v2-report-summary b { + color: #3b5879; + font-weight: 700; +} + +.v2-detail-section > .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; + border: 1px solid #dce5ef; + border-radius: 11px; + background: #fff; + box-shadow: 0 6px 20px rgba(31, 53, 80, .04); +} + +.v2-vehicle-detail .v2-source-evidence.is-compact .v2-source-evidence-trigger { + min-height: 56px; + padding: 10px 11px; +} + +.v2-vehicle-detail .v2-source-evidence.is-compact .v2-source-evidence-body { + padding: 10px 11px 12px; +} + +.v2-event-strip.semi-card { + min-height: 52px; + overflow: hidden; + border-color: #dce5ef; + border-radius: 11px; + background: rgba(255, 255, 255, .98); + box-shadow: 0 6px 20px rgba(31, 53, 80, .045); +} + +.v2-event-strip > .semi-card-body { + display: grid; + min-height: 50px; + grid-template-columns: auto minmax(150px, 1fr) auto auto; + align-items: center; + gap: 0; + padding: 0 13px; +} + +.v2-event-sync { + display: flex; + min-width: 0; + align-items: center; + gap: 9px; + padding-right: 14px; +} + +.v2-event-sync > span { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; + line-height: 1.15; +} + +.v2-event-sync > span strong { + color: #2a3c53; + font-size: 10px; +} + +.v2-event-sync > span small, +.v2-event-load > small, +.v2-event-strip time small { + color: #8a97a8; + font-size: 8px; + font-weight: 550; + white-space: nowrap; +} + +.v2-monitor-live-tag.semi-tag { + height: 26px; + gap: 5px; + padding-inline: 8px; + border-radius: 13px; + font-size: 9px; + font-weight: 700; +} + +.v2-monitor-live-tag .semi-tag-content, +.v2-refresh-cadence .semi-tag-content { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.v2-event-load { + display: flex; + min-width: 0; + flex-direction: column; + gap: 3px; + border-left: 1px solid #e5ebf2; + padding: 0 14px; + line-height: 1.15; +} + +.v2-event-load > span { + overflow: hidden; + color: #4f6075; + font-size: 9px; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-refresh-cadence { + display: flex; + min-width: 0; + align-items: center; + gap: 8px; + border-left: 1px solid #e5ebf2; + padding: 0 14px; +} + +.v2-refresh-cadence > span { + color: #64748b; + font-size: 9px; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.v2-refresh-cadence .semi-tag { + height: 24px; + border-radius: 12px; + font-size: 8px; + font-weight: 700; +} + +.v2-event-strip time { + display: flex; + min-width: 92px; + align-items: center; + justify-content: flex-end; + gap: 7px; + border-left: 1px solid #e5ebf2; + padding-left: 14px; + color: #8290a3; +} + +.v2-event-strip time > span { + display: flex; + align-items: flex-end; + flex-direction: column; + gap: 2px; + line-height: 1.1; +} + +.v2-event-strip time b { + color: #4c5e73; + font-size: 9px; + font-weight: 700; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +@media (max-width: 1180px) { + .v2-event-strip > .semi-card-body { + grid-template-columns: auto minmax(120px, 1fr) auto; + } + + .v2-event-load { + display: none; + } +} + +@media (max-width: 760px) { + .v2-vehicle-detail.semi-card { + padding: 16px 10px 14px; + } + + .v2-detail-shell-header { + top: -16px; + margin: -12px -10px 8px; + padding: 17px 10px 10px; + } + + .v2-detail-shell-header .v2-detail-controls { + top: 16px; + right: 10px; + } + + .v2-detail-actions a { + padding-inline: 11px; + } + + .v2-detail-section > .v2-workspace-panel-header { + min-height: 54px; + } + + .v2-event-strip > .semi-card-body { + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px 0; + padding: 9px 10px; + } + + .v2-event-sync { + padding-right: 8px; + } + + .v2-event-load { + display: none; + } + + .v2-refresh-cadence { + display: flex; + grid-column: 1 / -1; + grid-row: 2; + align-items: center; + justify-content: space-between; + border-top: 1px solid #edf1f5; + border-left: 0; + padding: 8px 0 0; + } + + .v2-refresh-cadence > span { + overflow: hidden; + text-overflow: ellipsis; + } + + .v2-event-strip time { + min-width: 82px; + border-left: 0; + padding-left: 8px; + } +}