refine Semi UI telemetry details
This commit is contained in:
@@ -16,6 +16,25 @@ export function telemetryQualityLabel(quality: string) {
|
||||
return '异常';
|
||||
}
|
||||
|
||||
export function telemetryQualityColor(quality: string) {
|
||||
return quality === 'good' ? 'green' : quality === 'stale' ? 'orange' : 'red';
|
||||
}
|
||||
|
||||
export function telemetryValueTypeLabel(value?: string) {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
if (!normalized) return '—';
|
||||
const labels: Record<string, string> = {
|
||||
array: '列表',
|
||||
boolean: '布尔值',
|
||||
dynamic: '动态值',
|
||||
number: '数值',
|
||||
numeric: '数值',
|
||||
object: '对象',
|
||||
string: '文本'
|
||||
};
|
||||
return labels[normalized] ?? value!;
|
||||
}
|
||||
|
||||
export function formatTelemetryTime(value?: string) {
|
||||
if (!value) return '—';
|
||||
const local = formatShanghaiDateTime(value);
|
||||
|
||||
@@ -484,7 +484,9 @@ test('keeps protocol telemetry independent and explains mileage semantics', asyn
|
||||
expect(complexValue).toHaveTextContent('4 个值');
|
||||
expect(complexValue).toHaveTextContent('3.21,3.22,3.23');
|
||||
fireEvent.click(complexValue);
|
||||
expect(await screen.findByRole('dialog', { name: '遥测字段完整数据' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('dialog', { name: '遥测字段详情' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-telemetry-detail-protocol')).toHaveClass('semi-tag-blue-light');
|
||||
expect(document.querySelector('.v2-telemetry-value-meta')).toHaveTextContent('数据类型列表');
|
||||
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/ }));
|
||||
@@ -515,6 +517,13 @@ 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(screen.getByRole('button', { name: '查看 GPS 速度 字段详情' })).toHaveClass('v2-telemetry-mobile-action');
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看 GPS 速度 字段详情' }));
|
||||
expect(await screen.findByRole('dialog', { name: '遥测字段详情' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-telemetry-value-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
||||
expect(document.querySelector('.v2-telemetry-value-meta')).toHaveTextContent('数据类型数值');
|
||||
expect(document.querySelector('.v2-telemetry-detail-protocol')).toHaveClass('semi-tag-cyan-light');
|
||||
expect(screen.getByRole('button', { name: '关闭遥测字段详情' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-vehicle-record-page')).toHaveClass('v2-vehicle-record-v3', 'is-mobile-layout');
|
||||
expect(screen.getByRole('group', { name: '车辆快捷操作' }).querySelectorAll('.semi-button')).toHaveLength(5);
|
||||
expect(screen.getByRole('group', { name: '车辆快捷操作' })).toHaveTextContent('切换车辆轨迹回放历史数据里程查询告警事件');
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { LatestTelemetryResponse, LatestTelemetryValue, QualityIssueRow, Ve
|
||||
import { usePlatformSession } from '../auth/AuthGate';
|
||||
import { canAdminister, hasMenu } from '../auth/session';
|
||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
|
||||
import { formatTelemetryTime, formatTelemetryValue, telemetryQualityLabel } from '../domain/telemetry';
|
||||
import { formatTelemetryTime, formatTelemetryValue, telemetryQualityColor, telemetryQualityLabel, telemetryValueTypeLabel } from '../domain/telemetry';
|
||||
import { formatZhNumber } from '../domain/formatters';
|
||||
import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols';
|
||||
import { isValidAMapCoordinate } from '../../integrations/amap';
|
||||
@@ -201,6 +201,9 @@ function telemetryValueDetail(item: LatestTelemetryValue) {
|
||||
function TelemetryValueCell({ item, onInspect }: { item: LatestTelemetryValue; onInspect?: (item: LatestTelemetryValue) => void }) {
|
||||
const presentation = telemetryValuePresentation(item);
|
||||
if (presentation.complex) {
|
||||
if (!onInspect) {
|
||||
return <div className="v2-telemetry-value is-complex is-static"><strong>{presentation.summary}</strong><small>{presentation.preview}</small></div>;
|
||||
}
|
||||
return <div className="v2-telemetry-value is-complex"><Button
|
||||
className="v2-telemetry-value-trigger"
|
||||
theme="borderless"
|
||||
@@ -215,8 +218,7 @@ function TelemetryValueCell({ item, onInspect }: { item: LatestTelemetryValue; o
|
||||
}
|
||||
|
||||
function TelemetryQualityCell({ item }: { item: LatestTelemetryValue }) {
|
||||
const color = item.quality === 'good' ? 'green' : item.quality === 'stale' ? 'orange' : 'red';
|
||||
return <div className="v2-telemetry-quality"><Tag color={color} type="light" size="small">{telemetryQualityLabel(item.quality)}</Tag><small title={item.qualityReason}>{item.qualityReason || '未提供质量说明'} · {formatZhNumber(item.freshnessSeconds, 0)}s</small></div>;
|
||||
return <div className="v2-telemetry-quality"><Tag color={telemetryQualityColor(item.quality)} type="light" size="small">{telemetryQualityLabel(item.quality)}</Tag><small title={item.qualityReason}>{item.qualityReason || '未提供质量说明'} · {formatZhNumber(item.freshnessSeconds, 0)}s</small></div>;
|
||||
}
|
||||
|
||||
function TelemetryTimeCell({ item }: { item: LatestTelemetryValue }) {
|
||||
@@ -239,7 +241,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
|
||||
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', '遥测字段完整数据', '关闭遥测字段完整数据');
|
||||
useSideSheetA11y(Boolean(inspectedValue), '.v2-telemetry-value-sidesheet', 'v2-telemetry-value-detail', '遥测字段详情', '关闭遥测字段详情');
|
||||
const indexed = useMemo(() => {
|
||||
const valuesByProtocolCategory = new Map<string, LatestTelemetryResponse['values']>();
|
||||
const sources = new Map<string, { protocol: string; endpoint?: string }>();
|
||||
@@ -277,9 +279,12 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
|
||||
? <Empty className="v2-telemetry-empty" title="暂无可展示字段" description={`该协议最近 ${data?.scannedFrames ?? 0} 帧没有可展示的标量遥测。`} />
|
||||
: mobileLayout
|
||||
? <List className="v2-telemetry-mobile-list" dataSource={visibleMetrics} split={false} renderItem={(item) => <List.Item className="v2-telemetry-mobile-item" key={telemetryRowKey(item)}>
|
||||
<header><TelemetryFieldCell item={item} /><TelemetryValueCell item={item} onInspect={setInspectedValue} /></header>
|
||||
<header><TelemetryFieldCell item={item} /><TelemetryValueCell item={item} /></header>
|
||||
<div><TelemetryQualityCell item={item} /><TelemetryTimeCell item={item} /></div>
|
||||
<TelemetrySourceCell item={item} />
|
||||
<footer className="v2-telemetry-mobile-source">
|
||||
<TelemetrySourceCell item={item} />
|
||||
<Button className="v2-telemetry-mobile-action" theme="borderless" type="primary" size="small" icon={<IconChevronRight />} iconPosition="right" aria-label={`查看 ${item.label} 字段详情`} onClick={() => setInspectedValue(item)}>查看详情</Button>
|
||||
</footer>
|
||||
</List.Item>} />
|
||||
: <div className="v2-telemetry-table-wrap"><Table
|
||||
className="v2-telemetry-table"
|
||||
@@ -305,7 +310,10 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
|
||||
<SideSheet
|
||||
className="v2-telemetry-value-sidesheet"
|
||||
visible={Boolean(inspectedValue)}
|
||||
width={520}
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 520}
|
||||
height={mobileLayout ? 'min(88dvh, 780px)' : undefined}
|
||||
aria-label="遥测字段详情"
|
||||
title={inspectedValue ? <div className="v2-telemetry-value-title"><strong>{inspectedValue.label}</strong><span>{inspectedValue.sourceField}</span></div> : null}
|
||||
onCancel={() => { setInspectedValue(undefined); setCopiedValue(false); }}
|
||||
footer={<div className="v2-telemetry-value-footer"><span>完整值仅按需渲染,不影响实时遥测刷新。</span><Button theme="solid" onClick={() => { setInspectedValue(undefined); setCopiedValue(false); }}>完成</Button></div>}
|
||||
@@ -316,10 +324,10 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
|
||||
size="small"
|
||||
row
|
||||
data={[
|
||||
{ key: '协议', value: protocolDisplayLabel(inspectedValue.protocol) },
|
||||
{ key: '数据类型', value: inspectedValue.valueType || '—' },
|
||||
{ key: '协议', value: <ProtocolTag className="v2-telemetry-detail-protocol" protocol={inspectedValue.protocol} compact /> },
|
||||
{ key: '数据类型', value: telemetryValueTypeLabel(inspectedValue.valueType) },
|
||||
{ key: '单位', value: inspectedValue.unit || '—' },
|
||||
{ key: '质量', value: telemetryQualityLabel(inspectedValue.quality) },
|
||||
{ key: '质量', value: <Tag className="v2-telemetry-detail-quality" color={telemetryQualityColor(inspectedValue.quality)} type="light" size="small">{telemetryQualityLabel(inspectedValue.quality)}</Tag> },
|
||||
{ key: '设备时间', value: formatTelemetryTime(inspectedValue.deviceTime) },
|
||||
{ key: '接收时间', value: formatTelemetryTime(inspectedValue.serverTime) }
|
||||
]}
|
||||
|
||||
@@ -90,6 +90,8 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources.VehiclePage).toContain('className="v2-telemetry-table"');
|
||||
expect(corePageSources.VehiclePage).toContain('<List className="v2-telemetry-mobile-list"');
|
||||
expect(corePageSources.VehiclePage).toContain('<List.Item className="v2-telemetry-mobile-item"');
|
||||
expect(corePageSources.VehiclePage).toContain('className="v2-telemetry-mobile-action"');
|
||||
expect(corePageSources.VehiclePage).toContain("placement={mobileLayout ? 'bottom' : 'right'}");
|
||||
expect(corePageSources.VehiclePage).toContain('<List');
|
||||
expect(corePageSources.VehiclePage).toContain('className="v2-event-list"');
|
||||
expect(corePageSources.VehiclePage).toContain('useMobileLayout');
|
||||
|
||||
@@ -8611,6 +8611,24 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
||||
}
|
||||
.v2-telemetry-value span { color: #7d8b9e; font-size: 9px; }
|
||||
.v2-telemetry-value.is-complex { display: block; }
|
||||
.v2-telemetry-value.is-complex.is-static {
|
||||
display: grid;
|
||||
max-width: 150px;
|
||||
justify-items: end;
|
||||
gap: 3px;
|
||||
text-align: right;
|
||||
}
|
||||
.v2-telemetry-value.is-complex.is-static strong,
|
||||
.v2-telemetry-value.is-complex.is-static small {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.v2-telemetry-value.is-complex.is-static small {
|
||||
color: #8492a4;
|
||||
font-size: 8px;
|
||||
}
|
||||
.v2-telemetry-value-trigger.semi-button {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
@@ -8698,6 +8716,12 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
||||
.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-detail-protocol.semi-tag,
|
||||
.v2-telemetry-detail-quality.semi-tag {
|
||||
min-height: 22px;
|
||||
margin: 0;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.v2-telemetry-value-detail > section {
|
||||
overflow: hidden;
|
||||
border: 1px solid #dfe6ef;
|
||||
@@ -8791,13 +8815,61 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
||||
border-top: 1px solid #edf1f5;
|
||||
padding-top: 9px;
|
||||
}
|
||||
.v2-telemetry-mobile-item > .v2-telemetry-source { display: flex; align-items: center; gap: 8px; }
|
||||
.v2-telemetry-mobile-item > .v2-telemetry-source small { min-width: 0; }
|
||||
.v2-telemetry-mobile-source {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #edf1f5;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.v2-telemetry-mobile-source > .v2-telemetry-source {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
.v2-telemetry-mobile-source > .v2-telemetry-source small {
|
||||
min-width: 0;
|
||||
}
|
||||
.v2-telemetry-mobile-action.semi-button {
|
||||
min-width: 76px;
|
||||
min-height: 30px;
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
border-radius: 7px;
|
||||
padding-inline: 7px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.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-sidesheet .semi-sidesheet-inner {
|
||||
width: 100vw !important;
|
||||
overflow: hidden;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #f4f7fb;
|
||||
box-shadow: 0 -18px 48px rgba(25,45,72,.18);
|
||||
}
|
||||
.v2-telemetry-value-sidesheet .semi-sidesheet-header {
|
||||
min-height: 68px;
|
||||
border-bottom-color: #dfe7f0;
|
||||
background: rgba(255,255,255,.98);
|
||||
padding: 11px 14px;
|
||||
}
|
||||
.v2-telemetry-value-sidesheet .semi-sidesheet-body {
|
||||
overflow: auto;
|
||||
background: #f4f7fb;
|
||||
padding: 10px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.v2-telemetry-value-sidesheet .semi-sidesheet-footer {
|
||||
border-top: 1px solid #dfe7f0;
|
||||
background: rgba(255,255,255,.98);
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.v2-telemetry-value-title strong { font-size: 15px; }
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user