Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
2026-07-16 17:14:34 +08:00

282 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMutation, useQuery } from '@tanstack/react-query';
import {
IconAlarm, IconArrowRight, IconBox, IconCalendar, IconClock, IconCopy,
IconMapPin, IconSearch, IconTickCircle
} from '@douyinfe/semi-icons';
import { FormEvent, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { LatestTelemetryResponse, QualityIssueRow, VehicleDetail, VehicleProfileSyncItem, VehicleProfileSyncResult, VehicleRealtimeRow } from '../../api/types';
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 { formatZhNumber } from '../domain/formatters';
import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from '../domain/profileSync';
import { isValidAMapCoordinate } from '../../integrations/amap';
import { FleetMap } from '../map/FleetMap';
import { InlineError, PageLoading } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext';
function fmt(value?: string) { return value?.trim() || '—'; }
function metric(value: number | undefined, fallback = '—') { return typeof value === 'number' && Number.isFinite(value) ? formatZhNumber(value, 1) : fallback; }
function availableMetric(value: number | undefined, available?: boolean) { return available === false ? '—' : metric(value); }
function issueTone(issue: QualityIssueRow) { return issue.severity === 'error' ? 'error' : 'warning'; }
function durationHours(seconds?: number | null) { return seconds == null ? '—' : `${formatZhNumber(seconds / 3600, 1)} 小时`; }
function localDateTime(value?: string) { return value ? value.slice(0, 16) : ''; }
const operationStatusLabels = { unknown: '待维护', active: '运营中', inactive: '停运', maintenance: '维保中', retired: '已退役' } as const;
const SINGLE_VEHICLE_REFRESH_MS = 10_000;
function ProfileSyncPanel({ onClose }: { onClose: () => void }) {
const [sourceSystem, setSourceSystem] = useState('');
const [sourceVersion, setSourceVersion] = useState('');
const [conflictPolicy, setConflictPolicy] = useState<'preserve' | 'overwrite'>('preserve');
const [items, setItems] = useState<VehicleProfileSyncItem[]>([]);
const [fileName, setFileName] = useState('');
const [parseError, setParseError] = useState('');
const sync = useMutation<VehicleProfileSyncResult, Error, boolean>({
mutationFn: (dryRun) => api.syncVehicleProfiles({ sourceSystem: sourceSystem.trim(), sourceVersion: sourceVersion.trim(), conflictPolicy, dryRun, items })
});
const readFile = async (file?: File) => {
sync.reset(); setItems([]); setFileName(file?.name ?? ''); setParseError('');
if (!file) return;
try { setItems(parseVehicleProfileSyncCSV(await file.text())); } catch (error) { setParseError(error instanceof Error ? error.message : 'CSV 解析失败'); }
};
const ready = sourceSystem.trim() !== '' && sourceVersion.trim() !== '' && items.length > 0 && !sync.isPending;
const issues = sync.data?.items.filter((item) => item.status.startsWith('conflict_') || item.status === 'missing_vehicle').slice(0, 20) ?? [];
const applied = sync.data && !sync.data.dryRun;
return <section className="v2-profile-sync-panel" aria-label="车辆主档批量同步">
<header><div><strong></strong><p>CSV 500 </p></div><button type="button" onClick={onClose}></button></header>
<div className="v2-profile-sync-fields">
<label><span></span><input value={sourceSystem} onChange={(event) => { setSourceSystem(event.target.value); sync.reset(); }} placeholder="例如 oem-tsp" maxLength={64} /></label>
<label><span></span><input value={sourceVersion} onChange={(event) => { setSourceVersion(event.target.value); sync.reset(); }} placeholder="例如 snapshot-20260714-01" maxLength={128} /></label>
<label><span></span><select value={conflictPolicy} onChange={(event) => { setConflictPolicy(event.target.value as 'preserve' | 'overwrite'); sync.reset(); }}><option value="preserve"></option><option value="overwrite"></option></select></label>
<label className="is-file"><span>CSV </span><input type="file" accept=".csv,text/csv" onChange={(event) => { void readFile(event.target.files?.[0]); }} /></label>
</div>
<p className="v2-profile-sync-format"><code>{vehicleProfileSyncCSVHeader}</code></p>
{fileName ? <p className="v2-profile-sync-file">{fileName} · {items.length} </p> : null}
{parseError ? <p className="v2-profile-sync-error">{parseError}</p> : null}
{sync.isError ? <p className="v2-profile-sync-error">{sync.error.message}</p> : null}
{sync.data ? <div className="v2-profile-sync-result">
<div><span><strong>{sync.data.received}</strong></span><span><strong>{sync.data.created}</strong></span><span><strong>{sync.data.updated}</strong></span><span><strong>{sync.data.unchanged}</strong></span><span><strong>{sync.data.conflicted}</strong></span><span><strong>{sync.data.missing}</strong></span></div>
{issues.length ? <ul>{issues.map((item) => <li key={item.vin}><b>{item.vin}</b><span>{item.status === 'missing_vehicle' ? '网关身份不存在' : item.status === 'conflict_source_version' ? '同来源版本内容不一致' : `现有来源 ${item.previousSource || '未知'} 已保护`}</span></li>)}</ul> : <p></p>}
</div> : null}
{conflictPolicy === 'overwrite' ? <p className="v2-profile-sync-warning"></p> : null}
<footer><button type="button" onClick={() => sync.mutate(true)} disabled={!ready}>{sync.isPending ? '处理中…' : '预演同步'}</button><button className="is-primary" type="button" onClick={() => sync.mutate(false)} disabled={!ready || !sync.data?.dryRun}>{applied ? '已完成写入' : '确认写入'}</button></footer>
</section>;
}
function VehicleSearch() {
const navigate = useNavigate();
const { session } = usePlatformSession();
const [keyword, setKeyword] = useState('');
const [syncOpen, setSyncOpen] = useState(false);
const submit = (event: FormEvent) => {
event.preventDefault();
const value = keyword.trim();
if (value) navigate(`/vehicles/${encodeURIComponent(value)}`);
};
return <section className={`v2-vehicle-search-page ${syncOpen ? 'has-sync-panel' : ''}`}>
<div className="v2-vehicle-search-card">
<span className="v2-search-hero-icon"><IconBox size="extra-large" /></span>
<h2></h2>
<p>VIN </p>
<form onSubmit={submit}>
<IconSearch /><input value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="输入车牌 / VIN / 终端手机号" autoFocus />
<button type="submit"> <IconArrowRight /></button>
</form>
{canAdminister(session) ? <button className="v2-profile-sync-open" type="button" onClick={() => setSyncOpen((value) => !value)}>{syncOpen ? '收起批量同步' : '批量同步主档'}</button> : null}
</div>
{syncOpen ? <ProfileSyncPanel onClose={() => setSyncOpen(false)} /> : null}
</section>;
}
function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; editable: boolean; onUpdated: () => void }) {
const profile = detail.profile;
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState({ modelName: '', vehicleType: '', companyName: '', operationStatus: 'unknown', accessProvider: '', firstAccessAt: '', runtimeHours: '' });
const save = useMutation({
mutationFn: () => api.updateVehicleProfile(detail.vin, {
modelName: draft.modelName.trim(), vehicleType: draft.vehicleType.trim(), companyName: draft.companyName.trim(),
operationStatus: draft.operationStatus as NonNullable<typeof profile>['operationStatus'], accessProvider: draft.accessProvider.trim(), firstAccessAt: draft.firstAccessAt,
runtimeSeconds: draft.runtimeHours.trim() === '' ? null : Math.round(Number(draft.runtimeHours) * 3600), version: profile?.version ?? 0
}),
onSuccess: () => { setEditing(false); onUpdated(); }
});
const startEditing = () => {
setDraft({ modelName: profile?.modelName ?? '', vehicleType: profile?.vehicleType ?? '', companyName: profile?.companyName ?? '', operationStatus: profile?.operationStatus ?? 'unknown', accessProvider: profile?.accessProvider ?? '', firstAccessAt: localDateTime(profile?.firstAccessAt), runtimeHours: profile?.runtimeSeconds == null ? '' : String(profile.runtimeSeconds / 3600) });
save.reset(); setEditing(true);
};
const submit = (event: FormEvent) => { event.preventDefault(); if (draft.runtimeHours === '' || Number.isFinite(Number(draft.runtimeHours))) save.mutate(); };
return <section className="v2-record-card v2-archive-card">
<header><strong></strong><span className="v2-profile-heading"> {profile?.completeness ?? 0}%{editable && !editing ? <button type="button" onClick={startEditing}></button> : null}</span></header>
{editing ? <form className="v2-profile-form" onSubmit={submit}>
<label><span></span><input maxLength={128} value={draft.modelName} onChange={(event) => setDraft({ ...draft, modelName: event.target.value })} /></label>
<label><span></span><input maxLength={64} value={draft.vehicleType} onChange={(event) => setDraft({ ...draft, vehicleType: event.target.value })} /></label>
<label><span></span><input maxLength={128} value={draft.companyName} onChange={(event) => setDraft({ ...draft, companyName: event.target.value })} /></label>
<label><span></span><select value={draft.operationStatus} onChange={(event) => setDraft({ ...draft, operationStatus: event.target.value })}>{Object.entries(operationStatusLabels).map(([value, label]) => <option value={value} key={value}>{label}</option>)}</select></label>
<label><span></span><input maxLength={128} value={draft.accessProvider} onChange={(event) => setDraft({ ...draft, accessProvider: event.target.value })} /></label>
<label><span></span><input type="datetime-local" value={draft.firstAccessAt} onChange={(event) => setDraft({ ...draft, firstAccessAt: event.target.value })} /></label>
<label><span></span><input type="number" min="0" step="0.1" value={draft.runtimeHours} onChange={(event) => setDraft({ ...draft, runtimeHours: event.target.value })} /></label>
{save.isError ? <p>{save.error.message}</p> : null}<footer><button type="button" onClick={() => setEditing(false)}></button><button className="is-primary" type="submit" disabled={save.isPending}>{save.isPending ? '保存中' : '保存档案'}</button></footer>
</form> : <><dl className="v2-record-list">
<div><dt> / </dt><dd>{[profile?.modelName, profile?.vehicleType].filter(Boolean).join(' / ') || '—'}</dd></div>
<div><dt></dt><dd>{fmt(profile?.companyName)}</dd></div>
<div><dt></dt><dd>{operationStatusLabels[profile?.operationStatus ?? 'unknown']}</dd></div>
<div><dt></dt><dd>{fmt(profile?.accessProvider)}</dd></div>
<div><dt></dt><dd>{fmt(profile?.firstAccessAt)}</dd></div>
<div><dt></dt><dd>{durationHours(profile?.runtimeSeconds)}</dd></div>
</dl><p className="v2-record-note"> {profile?.sourceSystem || '未配置'}{profile?.updatedAt ? ` · v${profile.version} · ${profile.updatedBy} 更新` : ''}</p></>}
</section>;
}
function Events({ detail }: { detail: VehicleDetail }) {
const events = [
...detail.quality.items.slice(0, 3).map((item) => ({ tone: issueTone(item), title: item.severity === 'error' ? '质量异常' : '质量提醒', detail: item.detail, time: item.lastSeen })),
...detail.sourceStatus.slice(0, 3).map((item) => ({ tone: item.online ? 'success' : 'muted', title: item.online ? '数据上报' : '来源离线', detail: `${item.protocol} · ${item.online ? '当前在线' : '暂无在线数据'}`, time: item.lastSeen }))
].slice(0, 5);
return <section className="v2-record-card v2-events-card">
<header><strong></strong><Link to={`/alerts?vin=${encodeURIComponent(detail.vin)}`}></Link></header>
<div className="v2-event-list">{events.length ? events.map((event, index) => <div className={`v2-event-row is-${event.tone}`} key={`${event.title}-${event.time}-${index}`}>
<span className="v2-event-icon">{event.tone === 'success' ? <IconTickCircle /> : <IconAlarm />}</span>
<div><strong>{event.title}</strong><p>{event.detail}</p></div><time>{fmt(event.time)}</time>
</div>) : <div className="v2-empty-compact"></div>}</div>
</section>;
}
function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryResponse; pending: boolean; error?: string }) {
const [selectedCategory, setSelectedCategory] = useState('vehicle');
const indexed = useMemo(() => {
const valuesByCategory = new Map<string, LatestTelemetryResponse['values']>();
const sources = new Map<string, { protocol: string; endpoint?: string }>();
for (const value of data?.values ?? []) {
const values = valuesByCategory.get(value.category);
if (values) values.push(value); else valuesByCategory.set(value.category, [value]);
const sourceKey = `${value.protocol}\u0000${value.sourceEndpoint ?? ''}`;
if (!sources.has(sourceKey)) sources.set(sourceKey, { protocol: value.protocol, endpoint: value.sourceEndpoint });
}
return { valuesByCategory, sources: [...sources.values()] };
}, [data]);
const categories = data?.categories ?? [];
const activeCategory = indexed.valuesByCategory.has(selectedCategory) ? selectedCategory : categories[0]?.key ?? '';
const visibleMetrics = indexed.valuesByCategory.get(activeCategory) ?? [];
return <section className="v2-record-card v2-telemetry-card">
<nav>{categories.map((item) => <button className={activeCategory === item.key ? 'is-active' : ''} onClick={() => setSelectedCategory(item.key)} type="button" key={item.key}>{item.label}<span>{item.count}</span></button>)}</nav>
<div className="v2-telemetry-list">
{pending ? <div className="v2-empty-compact"></div> : error ? <div className="v2-empty-compact is-error">{error}</div> : visibleMetrics.length ? visibleMetrics.map((item) => <div key={item.key}>
<span>{item.label}<small title={item.sourceField}>{item.sourceField} · {item.protocol}{item.sourceEndpoint ? ` · ${item.sourceEndpoint}` : ''}</small></span>
<strong>{formatTelemetryValue(item.value)} <em>{item.unit}</em></strong>
<time title={`设备时间 ${item.deviceTime || '缺失'};接收时间 ${item.serverTime || '缺失'}${item.qualityReason};帧 ${item.frameId}`}><i className={`is-${item.quality}`}>{telemetryQualityLabel(item.quality)}</i>{formatTelemetryTime(item.deviceTime || item.serverTime)}</time>
</div>) : <div className="v2-empty-compact"> {data?.scannedFrames ?? 0} </div>}
</div>
<footer><b></b>{indexed.sources.map((source) => <span key={`${source.protocol}-${source.endpoint ?? ''}`} title={source.endpoint}>{source.protocol}</span>)}<small> {data?.scannedFrames ?? 0} · {formatTelemetryTime(data?.asOf)}</small></footer>
</section>;
}
function CurrentVehicleAddress({ vehicle, fallback }: { vehicle?: VehicleRealtimeRow; fallback?: string }) {
const coordinate = vehicle && vehicle.locationAvailable !== false && isValidAMapCoordinate(vehicle.longitude, vehicle.latitude)
? { longitude: Number(vehicle.longitude.toFixed(4)), latitude: Number(vehicle.latitude.toFixed(4)) }
: undefined;
const coordinateKey = coordinate ? `${coordinate.longitude.toFixed(4)},${coordinate.latitude.toFixed(4)}` : '';
const [requestedKey, setRequestedKey] = useState('');
const requested = requestedKey ? requestedKey.split(',').map(Number) : [];
const address = useQuery({
queryKey: ['vehicle-detail-address', requestedKey],
enabled: requested.length === 2,
queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({
longitude: requested[0].toFixed(4),
latitude: requested[1].toFixed(4)
}), signal),
staleTime: 6 * 60 * 60_000,
gcTime: QUERY_MEMORY.highVolumeGcTime,
refetchOnWindowFocus: false,
retry: 1
});
const moved = Boolean(coordinateKey && requestedKey && coordinateKey !== requestedKey);
return <div className="v2-current-location">
<span><IconMapPin /><small></small><strong>{coordinate ? `${vehicle!.longitude.toFixed(6)}, ${vehicle!.latitude.toFixed(6)}` : '—'}</strong></span>
<span className="v2-current-address"><small></small>{!coordinate
? <strong></strong>
: !requestedKey
? <button type="button" onClick={() => setRequestedKey(coordinateKey)}></button>
: address.isFetching && !address.data
? <strong></strong>
: address.isError
? <button type="button" className="is-error" onClick={() => void address.refetch()}></button>
: <strong title={address.data?.formattedAddress}>{address.data?.formattedAddress || fallback || '暂无地址结果'}</strong>}
{moved ? <button type="button" onClick={() => setRequestedKey(coordinateKey)}> · </button> : null}
</span>
</div>;
}
function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, telemetryError, monitorReturn, onUpdated }: { detail: VehicleDetail; liveRealtime?: VehicleRealtimeRow; telemetry?: LatestTelemetryResponse; telemetryPending: boolean; telemetryError?: string; monitorReturn: string; onUpdated: () => void }) {
const { session } = usePlatformSession();
const [sourceEvidenceOpen, setSourceEvidenceOpen] = useState(false);
const realtime = liveRealtime ?? detail.realtimeSummary;
const identity = detail.identity;
const hasLocation = Boolean(realtime && realtime.locationAvailable !== false && isValidAMapCoordinate(realtime.longitude, realtime.latitude));
const mapVehicles = hasLocation && realtime ? [realtime] : [];
const lastMileage = detail.mileage.items[0];
return <div className="v2-vehicle-record-page">
<MonitorReturnBar />
<section className="v2-identity-band">
<div className="v2-identity-primary"><span className="v2-plate"><IconBox />{fmt(identity?.plate || realtime?.plate)}</span><span className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`}><i />{realtime?.online ? '在线' : '离线'}</span><small>VIN</small><b>{detail.vin}</b><button type="button" title="复制 VIN" onClick={() => navigator.clipboard?.writeText(detail.vin)}><IconCopy /></button></div>
<div className="v2-identity-meta"><div><small></small><strong>{fmt(identity?.oem || realtime?.oem)}</strong></div><div><small></small><p>{detail.sources.length ? detail.sources.map((source) => <span key={source}>{source}</span>) : <span></span>}</p></div></div>
<div className="v2-identity-actions">
{hasMenu(session, 'tracks') ? <Link to={withMonitorReturn(`/tracks?vin=${encodeURIComponent(detail.vin)}`, monitorReturn)}><IconMapPin /></Link> : null}
{hasMenu(session, 'history') ? <Link to={withMonitorReturn(`/history?vin=${encodeURIComponent(detail.vin)}`, monitorReturn)}><IconCalendar /></Link> : null}
{hasMenu(session, 'statistics') ? <Link to={withMonitorReturn(`/statistics?vins=${encodeURIComponent(detail.vin)}`, monitorReturn)}><IconClock /></Link> : null}
{hasMenu(session, 'alerts') ? <Link to={`/alerts?vin=${encodeURIComponent(detail.vin)}`}><IconAlarm /></Link> : null}
</div>
</section>
<section className="v2-record-card v2-live-card v2-live-overview">
<header><strong></strong><span><i className={realtime?.online ? 'is-online' : ''} />{realtime?.online ? '实时在线' : '当前离线'} · <IconClock />{fmt(realtime?.lastSeen || identity?.lastSeen)}</span></header>
<div className="v2-live-grid">
<div><small></small><strong>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}<em>km/h</em></strong></div>
<div><small>SOC</small><strong>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em>%</em></strong></div>
<div><small></small><button type="button" className="v2-live-source-link" title="展开全部里程来源" onClick={() => setSourceEvidenceOpen(true)}>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}<em>km</em></button></div>
<div><small></small><button type="button" className="v2-live-source-link" title="展开全部里程来源" onClick={() => setSourceEvidenceOpen(true)}>{availableMetric(realtime?.todayMileageKm ?? lastMileage?.dailyMileageKm, realtime?.todayMileageAvailable)}<em>km</em></button></div>
<div><small></small><strong className="is-text">{fmt(realtime?.primaryProtocol)}<em>{realtime?.locationSource ? ` · ${realtime.locationSource}` : ''}</em></strong></div>
<div><small></small><strong>{realtime?.onlineSourceCount ?? 0}<em> 线 / {detail.sourceStatus.length} </em></strong></div>
</div>
<CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} />
</section>
<VehicleSourceEvidencePanel vin={detail.vin} open={sourceEvidenceOpen} onOpenChange={setSourceEvidenceOpen} />
<div className="v2-record-grid">
<section className="v2-single-map-card"><FleetMap vehicles={mapVehicles} selectedVin={hasLocation ? detail.vin : undefined} onSelect={() => undefined} /><footer><button type="button" className="v2-map-source-link" title="展开全部位置来源" onClick={() => setSourceEvidenceOpen(true)}><IconMapPin />{hasLocation && realtime ? `实时坐标 ${realtime.longitude.toFixed(6)}, ${realtime.latitude.toFixed(6)}` : '暂无有效实时坐标'}{identity?.locationText ? ` · 档案区域 ${identity.locationText}` : ''}</button><time>{fmt(realtime?.lastSeen)}</time></footer></section>
<Events detail={detail} />
<TelemetryPanel data={telemetry} pending={telemetryPending} error={telemetryError} />
<Archive detail={detail} editable={canAdminister(session)} onUpdated={onUpdated} />
</div>
</div>;
}
export default function VehiclePage() {
const { vin } = useParams();
const [searchParams] = useSearchParams();
const monitorReturn = monitorReturnFromParams(searchParams);
const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime });
const resolvedVin = query.data?.lookupResolved ? query.data.vin : '';
const realtime = useQuery({
queryKey: ['vehicle-detail-realtime', resolvedVin],
queryFn: ({ signal }) => api.vehicleRealtime(new URLSearchParams({ keywords: resolvedVin, limit: '1', offset: '0' }), signal),
enabled: Boolean(resolvedVin),
staleTime: 5_000,
gcTime: QUERY_MEMORY.highVolumeGcTime,
refetchInterval: resolvedVin ? SINGLE_VEHICLE_REFRESH_MS : false,
...LIVE_QUERY_POLICY
});
const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY });
if (!vin) return <VehicleSearch />;
if (query.isPending) return <PageLoading />;
if (query.isError) return <div className="v2-page-error"><InlineError message={query.error instanceof Error ? query.error.message : '车辆档案加载失败'} onRetry={() => query.refetch()} /></div>;
if (!query.data.lookupResolved) return <section className="v2-not-found"><IconSearch size="extra-large" /><h2></h2><p>{vin}VIN </p><Link to="/vehicles"></Link></section>;
return <VehicleRecord detail={query.data} liveRealtime={realtime.data?.items[0]} telemetry={telemetry.data} telemetryPending={telemetry.isPending} telemetryError={telemetry.isError ? (telemetry.error instanceof Error ? telemetry.error.message : '请求失败') : undefined} monitorReturn={monitorReturn} onUpdated={() => { void query.refetch(); void realtime.refetch(); void telemetry.refetch(); }} />;
}