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 2a0722aa..c8285c09 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
@@ -356,13 +356,18 @@ test('keeps protocol telemetry independent and explains mileage semantics', asyn
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [{ ...initialRealtime, protocols: ['GB32960', 'JT808'] }], total: 1, limit: 1, offset: 0 });
vi.spyOn(api, 'latestTelemetry').mockResolvedValue({
vin: initialRealtime.vin,
- categories: [{ key: 'vehicle', label: '整车数据', count: 2 }],
+ categories: [{ key: 'vehicle', label: '整车数据', count: 3 }],
values: [
{
key: 'total_mileage_km', sourceField: 'gb32960.vehicle.total_mileage_km', label: '仪表盘总里程', unit: 'km',
category: 'vehicle', valueType: 'numeric', value: 1200, protocol: 'GB32960', frameId: 'gb', deviceTime: '', serverTime: '2026-07-16 10:00:00',
quality: 'good', qualityReason: '正常', freshnessSeconds: 1
},
+ {
+ key: 'cell_voltages', sourceField: 'gb32960.extended.cell_voltages', label: '单体电压列表', unit: 'V',
+ category: 'vehicle', valueType: 'array', value: [3.21, 3.22, 3.23, 3.24], protocol: 'GB32960', frameId: 'gb-array', deviceTime: '', serverTime: '2026-07-16 10:00:00',
+ quality: 'good', qualityReason: '正常', freshnessSeconds: 1, sourceEndpoint: '10.0.0.1:8080'
+ },
{
key: 'total_mileage_km', sourceField: 'jt808.location.total_mileage_km', label: 'GPS 总里程', unit: 'km',
category: 'vehicle', valueType: 'numeric', value: 1180, protocol: 'JT808', frameId: 'jt', deviceTime: '', serverTime: '2026-07-16 10:00:00',
@@ -383,6 +388,14 @@ test('keeps protocol telemetry independent and explains mileage semantics', asyn
expect(document.querySelector('.v2-event-empty.semi-empty')).toBeInTheDocument();
expect(screen.getByText('仪表盘总里程')).toBeInTheDocument();
expect(screen.queryByText('GPS 总里程')).not.toBeInTheDocument();
+ const complexValue = screen.getByRole('button', { name: '查看 单体电压列表 完整数据' });
+ expect(complexValue).toHaveClass('semi-button', 'v2-telemetry-value-trigger');
+ expect(complexValue).toHaveTextContent('4 个值');
+ expect(complexValue).toHaveTextContent('3.21,3.22,3.23');
+ fireEvent.click(complexValue);
+ expect(await screen.findByRole('dialog', { name: '遥测字段完整数据' })).toBeInTheDocument();
+ expect(document.querySelector('.v2-telemetry-value-detail pre')).toHaveTextContent('3.24');
+ expect(document.querySelector('.v2-telemetry-value-detail > footer strong')).toHaveTextContent('10.0.0.1:8080');
fireEvent.click(screen.getByRole('tab', { name: /JT\/T 808/ }));
expect(screen.getByRole('tab', { name: /JT\/T 808/, selected: true })).toBeInTheDocument();
expect(screen.getByText('GPS 总里程')).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 d8f68cc7..57375304 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
@@ -3,7 +3,7 @@ import {
IconAlarm, IconBox, IconCalendar, IconClock, IconCopy,
IconChevronRight, IconMapPin, IconSearch, IconTickCircle
} from '@douyinfe/semi-icons';
-import { Button, Card, Descriptions, Empty, Input, List, Select, Table, Tag } from '@douyinfe/semi-ui';
+import { Button, Card, Descriptions, Empty, Input, List, Select, SideSheet, Table, Tag } from '@douyinfe/semi-ui';
import { FormEvent, lazy, Suspense, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
@@ -22,6 +22,7 @@ import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext';
import { useMobileLayout } from '../hooks/useMobileLayout';
+import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
function fmt(value?: string) { return value?.trim() || '—'; }
function metric(value: number | undefined, fallback = '—') { return typeof value === 'number' && Number.isFinite(value) ? formatZhNumber(value, 1) : fallback; }
@@ -122,8 +123,72 @@ function TelemetryFieldCell({ item }: { item: LatestTelemetryValue }) {
return
{item.label}{item.sourceField}
;
}
-function TelemetryValueCell({ item }: { item: LatestTelemetryValue }) {
- return {formatTelemetryValue(item.value, item.displayValue)}{item.unit ? {item.unit} : null}
;
+type TelemetryValuePresentation = {
+ complex: boolean;
+ preview: string;
+ summary: string;
+};
+
+function tryParseTelemetryJSON(value: string) {
+ const trimmed = value.trim();
+ if (!(trimmed.startsWith('[') && trimmed.endsWith(']')) && !(trimmed.startsWith('{') && trimmed.endsWith('}'))) return undefined;
+ try { return JSON.parse(trimmed) as unknown; } catch { return undefined; }
+}
+
+function telemetryValuePresentation(item: LatestTelemetryValue): TelemetryValuePresentation {
+ const formatted = formatTelemetryValue(item.value, item.displayValue);
+ const parsed = typeof item.value === 'string'
+ ? tryParseTelemetryJSON(item.value)
+ : typeof item.displayValue === 'string'
+ ? tryParseTelemetryJSON(item.displayValue)
+ : item.value;
+ if (Array.isArray(parsed)) {
+ return {
+ complex: true,
+ preview: parsed.slice(0, 3).map((value) => String(value)).join(',') || '空数组',
+ summary: `${formatZhNumber(parsed.length)} 个值`
+ };
+ }
+ if (parsed && typeof parsed === 'object') {
+ const entries = Object.entries(parsed);
+ return {
+ complex: true,
+ preview: entries.slice(0, 2).map(([key]) => key).join(',') || '空对象',
+ summary: `${formatZhNumber(entries.length)} 个字段`
+ };
+ }
+ const detail = String(formatted);
+ return {
+ complex: detail.length > 42,
+ preview: detail.length > 54 ? `${detail.slice(0, 54)}…` : detail,
+ summary: detail.length > 42 ? `${formatZhNumber(detail.length)} 个字符` : detail
+ };
+}
+
+function telemetryValueDetail(item: LatestTelemetryValue) {
+ const parsed = typeof item.value === 'string'
+ ? tryParseTelemetryJSON(item.value)
+ : typeof item.displayValue === 'string'
+ ? tryParseTelemetryJSON(item.displayValue)
+ : item.value;
+ if (parsed && typeof parsed === 'object') return JSON.stringify(parsed, null, 2);
+ return String(formatTelemetryValue(item.value, item.displayValue));
+}
+
+function TelemetryValueCell({ item, onInspect }: { item: LatestTelemetryValue; onInspect?: (item: LatestTelemetryValue) => void }) {
+ const presentation = telemetryValuePresentation(item);
+ if (presentation.complex) {
+ return ;
+ }
+ return {presentation.summary}{item.unit ? {item.unit} : null}
;
}
function TelemetryQualityCell({ item }: { item: LatestTelemetryValue }) {
@@ -147,6 +212,11 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
const mobileLayout = useMobileLayout();
const [selectedProtocol, setSelectedProtocol] = useState('');
const [selectedCategory, setSelectedCategory] = useState('vehicle');
+ const [inspectedValue, setInspectedValue] = useState();
+ const [copiedValue, setCopiedValue] = useState(false);
+ const inspectedPresentation = useMemo(() => inspectedValue ? telemetryValuePresentation(inspectedValue) : undefined, [inspectedValue]);
+ const inspectedDetail = useMemo(() => inspectedValue ? telemetryValueDetail(inspectedValue) : '', [inspectedValue]);
+ useSideSheetA11y(Boolean(inspectedValue), '.v2-telemetry-value-sidesheet', 'v2-telemetry-value-detail', '遥测字段完整数据', '关闭遥测字段完整数据');
const indexed = useMemo(() => {
const valuesByProtocolCategory = new Map();
const sources = new Map();
@@ -172,7 +242,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
const protocolLabel = (protocol: string) => protocol === 'GB32960' ? 'GB/T 32960' : protocol === 'JT808' ? 'JT/T 808' : protocol === 'YUTONG_MQTT' ? '宇通 MQTT' : protocol;
const columns = [
{ title: '字段 / 协议映射', dataIndex: 'label', width: 260, render: (_: unknown, item: LatestTelemetryValue) => },
- { title: '当前值', dataIndex: 'value', width: 150, render: (_: unknown, item: LatestTelemetryValue) => },
+ { title: '当前值', dataIndex: 'value', width: 170, render: (_: unknown, item: LatestTelemetryValue) => },
{ title: '质量 / 新鲜度', dataIndex: 'quality', width: 170, render: (_: unknown, item: LatestTelemetryValue) => },
{ title: '设备 / 接收时间', dataIndex: 'deviceTime', width: 230, render: (_: unknown, item: LatestTelemetryValue) => },
{ title: '数据来源', dataIndex: 'protocol', width: 180, render: (_: unknown, item: LatestTelemetryValue) => }
@@ -185,7 +255,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
?
: mobileLayout
?
-
+
} />
@@ -195,7 +265,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
dataSource={visibleMetrics}
rowKey={telemetryRowKey}
pagination={false}
- scroll={{ x: 990 }}
+ scroll={{ x: 1010 }}
empty={null}
/>;
return
@@ -210,6 +280,42 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
{state}
+ {inspectedValue.label}{inspectedValue.sourceField} : null}
+ onCancel={() => { setInspectedValue(undefined); setCopiedValue(false); }}
+ footer={完整值仅按需渲染,不影响实时遥测刷新。
}
+ >
+ {inspectedValue ?
+
+
+
+
: null}
+
;
}
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css
index 59879529..58961cbf 100644
--- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css
+++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css
@@ -8427,6 +8427,40 @@ button, a { -webkit-tap-highlight-color: transparent; }
font-variant-numeric: tabular-nums;
}
.v2-telemetry-value span { color: #7d8b9e; font-size: 9px; }
+.v2-telemetry-value.is-complex { display: block; }
+.v2-telemetry-value-trigger.semi-button {
+ width: 100%;
+ height: auto;
+ min-height: 36px;
+ justify-content: flex-start;
+ border-radius: 7px;
+ padding: 4px 6px;
+ color: #315f9f;
+ text-align: left;
+}
+.v2-telemetry-value-trigger .semi-button-content {
+ display: grid;
+ width: 100%;
+ min-width: 0;
+ grid-template-columns: minmax(0,1fr) auto;
+ align-items: center;
+ gap: 7px;
+}
+.v2-telemetry-value-trigger .semi-button-content > span {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+}
+.v2-telemetry-value-trigger .semi-button-content strong,
+.v2-telemetry-value-trigger .semi-button-content small {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.v2-telemetry-value-trigger .semi-button-content strong { color: #245f9f; font-size: 11px; }
+.v2-telemetry-value-trigger .semi-button-content small { color: #8492a4; font-size: 8px; font-weight: 500; }
+.v2-telemetry-value-trigger .semi-icon { flex: 0 0 auto; color: #7d9ac2; font-size: 12px; }
+.v2-telemetry-value-trigger:hover { background: #edf5ff; }
.v2-telemetry-quality { justify-items: start; }
.v2-telemetry-quality .semi-tag { min-height: 22px; border-radius: 999px; font-size: 9px; font-weight: 650; }
.v2-telemetry-quality small,
@@ -8453,6 +8487,87 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-telemetry-empty .semi-empty-description { color: #8492a4; font-size: 10px; }
.v2-telemetry-mobile-list { display: none; }
.v2-telemetry-card > .semi-card-body > footer { min-height: 40px; margin-top: 10px; }
+.v2-telemetry-value-sidesheet .semi-sidesheet-inner { max-width: 100vw; }
+.v2-telemetry-value-sidesheet .semi-sidesheet-header { border-bottom: 1px solid #e3e9f1; padding: 18px 20px 15px; }
+.v2-telemetry-value-title { display: grid; min-width: 0; gap: 4px; }
+.v2-telemetry-value-title strong {
+ overflow: hidden;
+ color: #243a53;
+ font-size: 16px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.v2-telemetry-value-title span {
+ overflow: hidden;
+ color: #8391a3;
+ font: 9px ui-monospace,SFMono-Regular,Menlo,monospace;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.v2-telemetry-value-sidesheet .semi-sidesheet-body { background: #f6f8fb; padding: 16px; }
+.v2-telemetry-value-detail { display: grid; gap: 12px; }
+.v2-telemetry-value-meta.semi-descriptions {
+ border: 1px solid #dfe6ef;
+ border-radius: 10px;
+ background: #fff;
+ padding: 12px 14px;
+}
+.v2-telemetry-value-meta .semi-descriptions-item { min-width: 130px; padding-block: 5px; }
+.v2-telemetry-value-meta .semi-descriptions-key { color: #8a97a8; font-size: 9px; }
+.v2-telemetry-value-meta .semi-descriptions-value { color: #3d5067; font-size: 10px; font-weight: 650; }
+.v2-telemetry-value-detail > section {
+ overflow: hidden;
+ border: 1px solid #dfe6ef;
+ border-radius: 10px;
+ background: #fff;
+}
+.v2-telemetry-value-detail > section > header {
+ display: flex;
+ min-height: 54px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ border-bottom: 1px solid #e4e9f0;
+ padding: 9px 12px 9px 15px;
+}
+.v2-telemetry-value-detail > section > header > div { display: grid; min-width: 0; gap: 3px; }
+.v2-telemetry-value-detail > section > header strong { color: #30445c; font-size: 12px; }
+.v2-telemetry-value-detail > section > header small { color: #8a97a8; font-size: 9px; }
+.v2-telemetry-value-detail > section > header .semi-button { flex: 0 0 auto; }
+.v2-telemetry-value-detail pre {
+ max-height: min(54vh,560px);
+ margin: 0;
+ overflow: auto;
+ background: #f8fafc;
+ padding: 15px;
+ color: #31465e;
+ font: 10px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;
+ tab-size: 2;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+.v2-telemetry-value-detail > footer {
+ display: grid;
+ min-width: 0;
+ grid-template-columns: auto minmax(0,1fr);
+ align-items: center;
+ gap: 9px;
+ border: 1px solid #dfe6ef;
+ border-radius: 9px;
+ background: #fff;
+ padding: 10px 12px;
+}
+.v2-telemetry-value-detail > footer span { color: #8a97a8; font-size: 9px; }
+.v2-telemetry-value-detail > footer strong {
+ overflow: hidden;
+ color: #52667d;
+ font: 9px ui-monospace,SFMono-Regular,Menlo,monospace;
+ text-align: right;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.v2-telemetry-value-footer { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 14px; }
+.v2-telemetry-value-footer span { color: #7e8d9f; font-size: 9px; }
@media (max-width: 680px) {
.v2-record-grid { grid-template-rows: none; }
@@ -8483,6 +8598,8 @@ button, a { -webkit-tap-highlight-color: transparent; }
}
.v2-telemetry-mobile-item > header .v2-telemetry-value { justify-content: end; }
.v2-telemetry-mobile-item > header .v2-telemetry-value strong { font-size: 13px; }
+ .v2-telemetry-mobile-item > header .v2-telemetry-value.is-complex { width: min(142px,42vw); }
+ .v2-telemetry-mobile-item > header .v2-telemetry-value-trigger.semi-button { padding: 2px 4px; }
.v2-telemetry-mobile-item > div {
display: grid;
grid-template-columns: minmax(0,.8fr) minmax(0,1.2fr);
@@ -8495,6 +8612,15 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-telemetry-mobile-item > .v2-telemetry-source small { min-width: 0; }
.v2-telemetry-card > .semi-card-body > footer { flex-wrap: wrap; gap: 5px; margin: 3px 9px 9px; }
.v2-telemetry-card > .semi-card-body > footer small { width: 100%; margin: 3px 0 0; }
+ .v2-telemetry-value-sidesheet .semi-sidesheet-inner { width: 100vw !important; }
+ .v2-telemetry-value-sidesheet .semi-sidesheet-header { padding: 15px 16px 13px; }
+ .v2-telemetry-value-sidesheet .semi-sidesheet-body { padding: 12px; }
+ .v2-telemetry-value-meta .semi-descriptions-item { min-width: 108px; }
+ .v2-telemetry-value-detail > section > header { align-items: flex-start; flex-direction: column; }
+ .v2-telemetry-value-detail > section > header .semi-button { width: 100%; justify-content: center; }
+ .v2-telemetry-value-detail pre { max-height: 46vh; font-size: 9px; }
+ .v2-telemetry-value-footer { align-items: stretch; flex-direction: column; }
+ .v2-telemetry-value-footer .semi-button { width: 100%; justify-content: center; }
}
/* Semi UI track evidence: one list language for stops, events and source provenance. */