feat(platform): customerize realtime map workspace
This commit is contained in:
@@ -455,7 +455,7 @@ export default function App() {
|
|||||||
const pages: Record<PageKey, JSX.Element> = {
|
const pages: Record<PageKey, JSX.Element> = {
|
||||||
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
|
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
|
||||||
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
||||||
map: <Realtime title="实时地图" description="以车辆服务为中心查看实时位置、在线状态、来源一致性和高德地图接入状态" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
||||||
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
||||||
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onQueryChange={updateVehicleDetailQuery} />,
|
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onQueryChange={updateVehicleDetailQuery} />,
|
||||||
history: <History mode="trajectory" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
history: <History mode="trajectory" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
||||||
|
|||||||
@@ -471,6 +471,7 @@ async function copyText(value: string, label: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Realtime({
|
export function Realtime({
|
||||||
|
mode = 'realtime',
|
||||||
title = '实时监控',
|
title = '实时监控',
|
||||||
description = '以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态',
|
description = '以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态',
|
||||||
onOpenVehicle,
|
onOpenVehicle,
|
||||||
@@ -479,6 +480,7 @@ export function Realtime({
|
|||||||
onFiltersChange,
|
onFiltersChange,
|
||||||
initialFilters = {}
|
initialFilters = {}
|
||||||
}: {
|
}: {
|
||||||
|
mode?: 'realtime' | 'map';
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
onOpenVehicle: (vin: string, protocol?: string) => void;
|
onOpenVehicle: (vin: string, protocol?: string) => void;
|
||||||
@@ -718,6 +720,156 @@ export function Realtime({
|
|||||||
const selectRealtimeMapPoint = (point: VehicleMapPoint) => {
|
const selectRealtimeMapPoint = (point: VehicleMapPoint) => {
|
||||||
setSelectedMapPointId(point.id);
|
setSelectedMapPointId(point.id);
|
||||||
};
|
};
|
||||||
|
const mapAttentionRows = rows
|
||||||
|
.filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row)))
|
||||||
|
.sort((a, b) => serviceStatusWeight(b) - serviceStatusWeight(a) || String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')))
|
||||||
|
.slice(0, 8);
|
||||||
|
const mapFleetKpis = [
|
||||||
|
{ label: '车辆总数', value: pagination.total.toLocaleString(), color: 'blue' as const, helper: `当前页 ${rows.length.toLocaleString()} 辆` },
|
||||||
|
{ label: '在线车辆', value: onlineCount.toLocaleString(), color: onlineCount > 0 ? 'green' as const : 'orange' as const, helper: `在线率 ${formatPercent(onlineRate)}` },
|
||||||
|
{ label: '有效定位', value: locatedCount.toLocaleString(), color: locatedCount > 0 ? 'green' as const : 'orange' as const, helper: `定位率 ${formatPercent(locatedRate)}` },
|
||||||
|
{ label: '需关注', value: mapAttentionRows.length.toLocaleString(), color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, helper: `${staleCount.toLocaleString()} 辆更新超时` }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (mode === 'map') {
|
||||||
|
return (
|
||||||
|
<div className="vp-page vp-map-page">
|
||||||
|
<PageHeader title={title} description={description} />
|
||||||
|
<div className="vp-map-commandbar">
|
||||||
|
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
|
||||||
|
applyFilters(values as Record<string, string>);
|
||||||
|
}}>
|
||||||
|
<Form.Input field="keyword" label="车辆" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
|
||||||
|
<Form.Select field="protocol" label="数据来源" placeholder="全部来源" style={{ width: 160 }}>
|
||||||
|
<Select.Option value="GB32960">GB32960</Select.Option>
|
||||||
|
<Select.Option value="JT808">JT808</Select.Option>
|
||||||
|
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
|
||||||
|
</Form.Select>
|
||||||
|
<Form.Select field="online" label="状态" placeholder="全部车辆" style={{ width: 130 }}>
|
||||||
|
<Select.Option value="online">在线</Select.Option>
|
||||||
|
<Select.Option value="offline">离线</Select.Option>
|
||||||
|
</Form.Select>
|
||||||
|
<Space>
|
||||||
|
<Button htmlType="submit" theme="solid" type="primary">刷新地图</Button>
|
||||||
|
<Button onClick={() => applyFilters({})}>重置</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
<Space wrap className="vp-map-refresh-actions">
|
||||||
|
<Tag color={autoRefresh ? 'green' : 'grey'}>{autoRefresh ? `自动刷新 ${refreshIntervalSeconds}秒` : '自动刷新已暂停'}</Tag>
|
||||||
|
<Typography.Text type="secondary">最后刷新:{lastRefreshAt || '等待首次刷新'}</Typography.Text>
|
||||||
|
<Button size="small" theme="light" loading={loading} onClick={() => load(filters, pagination.currentPage, pagination.pageSize)}>立即刷新</Button>
|
||||||
|
<Button size="small" theme="light" onClick={() => setAutoRefresh((value) => !value)}>{autoRefresh ? '暂停刷新' : '开启刷新'}</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="vp-map-kpi-strip">
|
||||||
|
{mapFleetKpis.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.label}
|
||||||
|
type="button"
|
||||||
|
className="vp-map-kpi-item"
|
||||||
|
onClick={() => {
|
||||||
|
if (item.label === '在线车辆') applyFilters({ ...filters, online: 'online' });
|
||||||
|
if (item.label === '需关注') applyFilters({ ...filters, serviceStatus: 'degraded' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag color={item.color}>{item.label}</Tag>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
<span>{item.helper}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="vp-map-workspace">
|
||||||
|
<section className="vp-map-canvas-panel">
|
||||||
|
<div className="vp-map-canvas-toolbar">
|
||||||
|
<Space wrap>
|
||||||
|
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
|
||||||
|
<Tag color="blue">{locatedCount.toLocaleString()} 辆有效定位</Tag>
|
||||||
|
{filterSummary.map((item) => <Tag key={item} color="grey">{item}</Tag>)}
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<Button size="small" theme="light" onClick={exportRealtime}>导出当前车辆</Button>
|
||||||
|
<Button size="small" theme="light" disabled={!onOpenQuality} onClick={() => onOpenQuality?.({ serviceStatus: 'degraded' })}>查看告警</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<VehicleMap
|
||||||
|
points={mapPoints}
|
||||||
|
maxFallbackPoints={160}
|
||||||
|
selectedId={selectedMapPointKey}
|
||||||
|
onPointSelect={selectRealtimeMapPoint}
|
||||||
|
heightClassName="vp-map-customer-canvas"
|
||||||
|
fallbackLabel="地图加载中,先显示车辆坐标态势"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside className="vp-map-customer-side">
|
||||||
|
{selectedMapRow ? (
|
||||||
|
<div className="vp-map-customer-card vp-map-selected-card">
|
||||||
|
<div className="vp-map-card-title">选中车辆</div>
|
||||||
|
<Space wrap>
|
||||||
|
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
|
||||||
|
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).detail}</Tag>
|
||||||
|
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知来源'}</Tag>
|
||||||
|
</Space>
|
||||||
|
<Typography.Title heading={5} style={{ margin: '8px 0 0' }}>
|
||||||
|
{selectedMapRow.plate || selectedMapRow.vin}
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Text type="tertiary">{selectedMapRow.vin}</Typography.Text>
|
||||||
|
<div className="vp-map-selected-metrics">
|
||||||
|
<span>速度</span><strong>{selectedMapRow.speedKmh ?? '-'} km/h</strong>
|
||||||
|
<span>SOC</span><strong>{selectedMapRow.socPercent ?? '-'}%</strong>
|
||||||
|
<span>总里程</span><strong>{selectedMapRow.totalMileageKm ?? '-'} km</strong>
|
||||||
|
<span>最后上报</span><strong>{selectedMapRow.lastSeen || '-'}</strong>
|
||||||
|
</div>
|
||||||
|
<Space wrap>
|
||||||
|
<Button size="small" theme="solid" type="primary" onClick={() => onOpenVehicle(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}>车辆档案</Button>
|
||||||
|
<Button size="small" onClick={() => onOpenHistory?.(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}>轨迹回放</Button>
|
||||||
|
<Button size="small" disabled={!isValidCoordinate(selectedMapRow)} onClick={() => window.open(amapMarkerURL(selectedMapRow), '_blank', 'noopener,noreferrer')}>高德坐标</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="vp-map-customer-card">
|
||||||
|
<div className="vp-map-card-title">关注车辆</div>
|
||||||
|
{mapAttentionRows.length === 0 ? (
|
||||||
|
<Typography.Text type="tertiary">当前筛选下暂无需要关注的车辆。</Typography.Text>
|
||||||
|
) : mapAttentionRows.map((row) => {
|
||||||
|
const status = vehicleServiceStatus(row);
|
||||||
|
return (
|
||||||
|
<button key={`${row.vin}-${row.primaryProtocol}-map-attention`} type="button" className="vp-map-attention-row" onClick={() => selectRealtimeRow(row)}>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>
|
||||||
|
<Typography.Text type="tertiary" size="small">{row.vin}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Tag color={status.color}>{status.label}</Tag>
|
||||||
|
<Typography.Text type="secondary" size="small">{realtimeIssueLabels(row).join(';')}</Typography.Text>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="vp-map-customer-card">
|
||||||
|
<div className="vp-map-card-title">车辆列表</div>
|
||||||
|
{mapServiceRows.length === 0 ? (
|
||||||
|
<Typography.Text type="tertiary">暂无可定位车辆。</Typography.Text>
|
||||||
|
) : mapServiceRows.map((row) => (
|
||||||
|
<button
|
||||||
|
key={`${row.vin}-${row.primaryProtocol}-map-list`}
|
||||||
|
type="button"
|
||||||
|
className={`vp-map-vehicle-row ${selectedMapRow?.vin === row.vin ? 'vp-map-vehicle-row-selected' : ''}`}
|
||||||
|
onClick={() => selectRealtimeRow(row)}
|
||||||
|
>
|
||||||
|
<span>{row.plate || row.vin}</span>
|
||||||
|
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vp-page">
|
<div className="vp-page">
|
||||||
|
|||||||
@@ -778,6 +778,195 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
height: 380px;
|
height: 380px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-map-page {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-commandbar {
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: var(--vp-surface);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-refresh-actions {
|
||||||
|
min-height: 32px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-kpi-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-kpi-item {
|
||||||
|
min-height: 96px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: #fbfcff;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
align-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-kpi-item:hover,
|
||||||
|
.vp-map-kpi-item:focus-visible {
|
||||||
|
border-color: rgba(22, 100, 255, 0.42);
|
||||||
|
background: #f5f9ff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-kpi-item strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-kpi-item span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-workspace {
|
||||||
|
min-height: 680px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 340px;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-canvas-panel {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: var(--vp-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-canvas-toolbar {
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--vp-border);
|
||||||
|
background: #fbfcff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-customer-canvas {
|
||||||
|
height: 626px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-customer-side {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-customer-card {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: var(--vp-surface);
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-selected-card {
|
||||||
|
border-color: rgba(22, 100, 255, 0.28);
|
||||||
|
background: #f5f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-card-title {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-selected-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-selected-metrics span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-selected-metrics strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-attention-row,
|
||||||
|
.vp-map-vehicle-row {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius-sm);
|
||||||
|
background: #fbfcff;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-attention-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-attention-row > div {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-vehicle-row {
|
||||||
|
min-height: 42px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-vehicle-row span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-attention-row:hover,
|
||||||
|
.vp-map-attention-row:focus-visible,
|
||||||
|
.vp-map-vehicle-row:hover,
|
||||||
|
.vp-map-vehicle-row:focus-visible,
|
||||||
|
.vp-map-vehicle-row-selected {
|
||||||
|
border-color: rgba(22, 100, 255, 0.5);
|
||||||
|
background: #f5f9ff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-map-dot {
|
.vp-map-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@@ -2577,6 +2766,8 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-realtime-impact-board,
|
.vp-realtime-impact-board,
|
||||||
.vp-realtime-impact-grid,
|
.vp-realtime-impact-grid,
|
||||||
.vp-source-coverage-grid,
|
.vp-source-coverage-grid,
|
||||||
|
.vp-map-kpi-strip,
|
||||||
|
.vp-map-workspace,
|
||||||
.vp-runbook-grid,
|
.vp-runbook-grid,
|
||||||
.vp-workbench-grid,
|
.vp-workbench-grid,
|
||||||
.vp-focus-service,
|
.vp-focus-service,
|
||||||
@@ -2624,6 +2815,16 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-map-commandbar,
|
||||||
|
.vp-map-canvas-toolbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-map-customer-canvas {
|
||||||
|
height: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-stat-closure-action {
|
.vp-stat-closure-action {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -191,8 +191,9 @@ test('exposes AMap operations shortcuts when map key is configured', async () =>
|
|||||||
expect(await screen.findByText((content) => content.includes('platform-20260704153357'))).toBeInTheDocument();
|
expect(await screen.findByText((content) => content.includes('platform-20260704153357'))).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' }));
|
||||||
expect(window.location.hash).toBe('#/map');
|
expect(window.location.hash).toBe('#/map');
|
||||||
expect(await screen.findByRole('heading', { name: '实时地图' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '车辆实时地图' })).toBeInTheDocument();
|
||||||
expect(screen.getByText('地图车辆队列')).toBeInTheDocument();
|
expect(screen.getByText('关注车辆')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('车辆列表')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' }));
|
||||||
expect(window.location.hash).toBe('#/realtime');
|
expect(window.location.hash).toBe('#/realtime');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' }));
|
||||||
@@ -8968,11 +8969,13 @@ test('shows realtime freshness status for recently updated and stale vehicles',
|
|||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByRole('heading', { name: '实时地图' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '车辆实时地图' })).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('数据新鲜').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('数据新鲜').length).toBeGreaterThan(0);
|
||||||
expect(screen.getAllByText('更新超时').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('更新超时').length).toBeGreaterThan(0);
|
||||||
expect(screen.getByText('超时车辆')).toBeInTheDocument();
|
expect(screen.getByText('关注车辆')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1 辆超过 5 分钟未更新。')).toBeInTheDocument();
|
expect(screen.getByText('选中车辆')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('车辆列表')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('1 辆更新超时')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('exports current realtime vehicles as CSV', async () => {
|
test('exports current realtime vehicles as CSV', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user