feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -344,7 +344,9 @@ test('recovers in place after a transient AMap failure and renders data that arr
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /重新加载地图/ }));
|
||||
const retry = screen.getByRole('button', { name: /重新加载地图/ });
|
||||
expect(retry).toHaveClass('semi-button', 'v2-map-retry-action');
|
||||
fireEvent.click(retry);
|
||||
expect(setData).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
@@ -520,6 +522,23 @@ test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data'
|
||||
expect(Math.abs(bounds[3] - 24)).toBeLessThan(0.0005);
|
||||
});
|
||||
|
||||
test('drops a wrapped world viewport instead of emitting invalid WGS-84 bounds', async () => {
|
||||
getZoom.mockReturnValue(3);
|
||||
getBounds.mockReturnValue({
|
||||
getSouthWest: () => ({ getLng: () => -80, getLat: () => -20 }),
|
||||
getNorthEast: () => ({ getLng: () => 260, getLat: () => 70 })
|
||||
});
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||
const onViewportChange = vi.fn();
|
||||
|
||||
render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} onViewportChange={onViewportChange} />);
|
||||
|
||||
await waitFor(() => expect(onViewportChange).toHaveBeenCalled());
|
||||
const viewport = onViewportChange.mock.calls[onViewportChange.mock.calls.length - 1]?.[0] as { zoom: number; bounds: string };
|
||||
expect(viewport).toEqual({ zoom: 3, bounds: '' });
|
||||
});
|
||||
|
||||
test('renders one selected plate and smoothly follows it until the map is dragged', async () => {
|
||||
useFastAnimationFrames();
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
@@ -702,7 +721,8 @@ test('renders crisp province totals and drills into the existing cluster layer',
|
||||
expect(setData).toHaveBeenLastCalledWith([]);
|
||||
const provinceIndex = markerOptions.findIndex((options) => String(options.content).includes('广东省'));
|
||||
expect(String(markerOptions[provinceIndex]?.content)).toContain('<strong>3</strong>');
|
||||
expect(String(markerOptions[provinceIndex]?.content)).toContain('在线 2');
|
||||
expect(String(markerOptions[provinceIndex]?.content)).toContain('<span>广东</span>');
|
||||
expect(String(markerOptions[provinceIndex]?.content)).not.toContain('在线');
|
||||
|
||||
markerInstances[provinceIndex]?.handlers.get('click')?.();
|
||||
expect(setZoomAndCenter).toHaveBeenLastCalledWith(7, [113.266, 23.132]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconEyeClosed, IconEyeOpened, IconMapPin, IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { IconEyeClosed, IconEyeOpened, IconMapPin } from '@douyinfe/semi-icons';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getAMapConfig, isAMapConfigured } from '../../config/appConfig';
|
||||
import {
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
type AMapOverlay
|
||||
} from '../../integrations/amap';
|
||||
import type { MonitorMapProvincePoint, MonitorMapResponse, VehicleRealtimeRow } from '../../api/types';
|
||||
import { vehicleStatus } from '../domain/monitor';
|
||||
import { normalizeMonitorBounds, vehicleStatus } from '../domain/monitor';
|
||||
import type { MonitorViewport } from '../hooks/useMonitorData';
|
||||
import { MapRetryAction } from '../shared/RecoveryActions';
|
||||
|
||||
const COLORS = ['#12a46f', '#9aa6b7', '#1677ff', '#f59e0b', '#ef4444'];
|
||||
const CLUSTER_VISUAL_CACHE_LIMIT = 256;
|
||||
@@ -78,11 +79,9 @@ function viewportFromMap(map: AMapMap): MonitorViewport | null {
|
||||
];
|
||||
const longitudes = wgsCorners.map(([longitude]) => longitude);
|
||||
const latitudes = wgsCorners.map(([, latitude]) => latitude);
|
||||
return {
|
||||
zoom,
|
||||
bounds: [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)]
|
||||
.map((value) => value.toFixed(6)).join(',')
|
||||
};
|
||||
const viewportBounds = [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)]
|
||||
.map((value) => value.toFixed(6)).join(',');
|
||||
return { zoom, bounds: normalizeMonitorBounds(viewportBounds) };
|
||||
}
|
||||
|
||||
function viewportCenter(viewport?: MonitorViewport) {
|
||||
@@ -757,13 +756,12 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
existing.marker.off?.('click', existing.click);
|
||||
existing.marker.setMap?.(null);
|
||||
}
|
||||
const onlinePercent = province.count ? Math.round(province.online / province.count * 100) : 0;
|
||||
const displayName = compactMarkers ? compactProvinceName(province.name) : province.name;
|
||||
const displayName = compactProvinceName(province.name);
|
||||
const marker = new AMap.Marker({
|
||||
position: [province.longitude, province.latitude],
|
||||
offset: new AMap.Pixel(0, 0),
|
||||
zIndex: 180,
|
||||
content: `<div class="v2-province-placement" style="--province-x:${placement.dx}px;--province-y:${placement.dy}px;--province-line:${connectorLength}px;--province-angle:${connectorAngle}deg"><i class="v2-province-anchor"></i>${connectorLength ? '<i class="v2-province-connector"></i>' : ''}<button type="button" tabindex="-1" class="v2-province-marker${compactMarkers ? ' is-compact' : ''}" aria-label="${escapeHtml(province.name)} ${province.count} 辆"><span>${escapeHtml(displayName)}</span><strong>${province.count.toLocaleString('zh-CN')}</strong><small>在线 ${province.online.toLocaleString('zh-CN')}</small><i><b style="width:${onlinePercent}%"></b></i></button></div>`
|
||||
content: `<div class="v2-province-placement" style="--province-x:${placement.dx}px;--province-y:${placement.dy}px;--province-line:${connectorLength}px;--province-angle:${connectorAngle}deg"><i class="v2-province-anchor"></i>${connectorLength ? '<i class="v2-province-connector"></i>' : ''}<button type="button" tabindex="-1" class="v2-province-marker${compactMarkers ? ' is-compact' : ''}" aria-label="${escapeHtml(province.name)} ${province.count} 辆"><span>${escapeHtml(displayName)}</span><strong>${province.count.toLocaleString('zh-CN')}</strong></button></div>`
|
||||
});
|
||||
const click = () => map.setZoomAndCenter?.(PROVINCE_DRILL_ZOOM, [province.longitude, province.latitude]);
|
||||
marker.on?.('click', click);
|
||||
@@ -1053,7 +1051,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
<div className={`v2-map-state is-${state}`}>
|
||||
{state === 'loading' ? <><span className="v2-spinner" />高德地图加载中</> : null}
|
||||
{state === 'fallback' ? `地图未配置,当前已载入 ${renderedPointCount} 个有效坐标` : null}
|
||||
{state === 'error' ? <><span>地图加载失败,请检查高德 Key、域名白名单和网络</span><button type="button" onClick={() => setLoadAttempt((value) => value + 1)}><IconRefresh />重新加载地图</button></> : null}
|
||||
{state === 'error' ? <><span>地图加载失败,请检查高德 Key、域名白名单和网络</span><MapRetryAction onRetry={() => setLoadAttempt((value) => value + 1)} /></> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="v2-map-legend" aria-label="车辆状态图例">
|
||||
|
||||
@@ -128,7 +128,9 @@ test('defers the map runtime until valid data exists, retries a transient failur
|
||||
onFollowChange={() => undefined}
|
||||
/>);
|
||||
expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /重新加载地图/ }));
|
||||
const retry = screen.getByRole('button', { name: /重新加载地图/ });
|
||||
expect(retry).toHaveClass('semi-button', 'v2-map-retry-action');
|
||||
fireEvent.click(retry);
|
||||
await waitFor(() => expect(mapInstances).toHaveLength(1));
|
||||
expect(load).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { HistoryLocationRow, TrackStop } from '../../api/types';
|
||||
import { getAMapConfig, isAMapConfigured } from '../../config/appConfig';
|
||||
import { isValidAMapCoordinate, loadAMap, wgs84ToGcj02, type AMapLike, type AMapMap, type AMapOverlay } from '../../integrations/amap';
|
||||
import { MapRetryAction } from '../shared/RecoveryActions';
|
||||
|
||||
function markerContent(kind: 'start' | 'end' | 'stop' | 'current', label?: string) {
|
||||
if (kind === 'current') return '<div class="v2-track-current-marker" aria-hidden="true"><i></i><span></span></div>';
|
||||
@@ -153,7 +153,7 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, follow
|
||||
{state === 'loading' ? <><span className="v2-spinner" />轨迹地图加载中</> : null}
|
||||
{state === 'idle' ? '当前轨迹没有可展示的有效坐标' : null}
|
||||
{state === 'fallback' ? `地图未配置,已载入 ${valid.length} 个有效轨迹点` : null}
|
||||
{state === 'error' ? <><span>地图加载失败,请检查高德地图配置或网络</span><button type="button" onClick={() => setLoadAttempt((value) => value + 1)}><IconRefresh />重新加载地图</button></> : null}
|
||||
{state === 'error' ? <><span>地图加载失败,请检查高德地图配置或网络</span><MapRetryAction onRetry={() => setLoadAttempt((value) => value + 1)} /></> : null}
|
||||
</div> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user