feat(platform): advance vehicle operations center

This commit is contained in:
lingniu
2026-07-04 16:22:05 +08:00
parent 775073a95c
commit 33da8ba5a4
6 changed files with 262 additions and 18 deletions

View File

@@ -67,6 +67,10 @@ function isValidCoordinate(row: VehicleRealtimeRow) {
return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}
function realtimeMapPointId(row: VehicleRealtimeRow, index = 0) {
return row.vin?.trim() || `${row.primaryProtocol || 'source'}-${index}`;
}
function amapMarkerURL(row: VehicleRealtimeRow) {
const name = encodeURIComponent(row.plate || row.vin || '车辆位置');
return `https://uri.amap.com/marker?position=${row.longitude},${row.latitude}&name=${name}&src=lingniu-vehicle-platform`;
@@ -191,6 +195,7 @@ export function Realtime({
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
const [selectedMapPointId, setSelectedMapPointId] = useState('');
const amapConfig = getAMapConfig();
const runtime = opsHealth?.runtime;
const amapConfigured = runtime?.amapWebJsConfigured ?? isAMapConfigured(amapConfig);
@@ -267,8 +272,10 @@ export function Realtime({
return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''));
})
.slice(0, 5);
const defaultMapRow = mapServiceRows[0] ?? rows.find((row) => isValidCoordinate(row) && canOpenVehicle(row.vin));
const selectedMapRow = rows.find((row, index) => realtimeMapPointId(row, index) === selectedMapPointId) ?? defaultMapRow;
const mapPoints: VehicleMapPoint[] = rows.map((row, index) => ({
id: row.vin || `${row.primaryProtocol}-${index}`,
id: realtimeMapPointId(row, index),
label: row.plate || row.vin || 'unknown',
longitude: row.longitude,
latitude: row.latitude,
@@ -381,6 +388,8 @@ export function Realtime({
<VehicleMap
points={mapPoints}
maxFallbackPoints={80}
selectedId={selectedMapRow ? realtimeMapPointId(selectedMapRow, rows.indexOf(selectedMapRow)) : selectedMapPointId}
onPointSelect={(point) => setSelectedMapPointId(point.id)}
fallbackLabel="高德地图未配置,显示实时坐标预览"
/>
</div>
@@ -400,6 +409,31 @@ export function Realtime({
<Typography.Text type="secondary">
Web JS Key Key
</Typography.Text>
{selectedMapRow ? (
<div className="vp-selected-vehicle-panel">
<div className="vp-map-service-queue-title"></div>
<Space wrap>
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={selectedMapRow.online ? 'green' : 'orange'}>{selectedMapRow.online ? '在线' : '离线'}</Tag>
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知来源'}</Tag>
</Space>
<Typography.Title heading={6} style={{ margin: '8px 0 0' }}>
{selectedMapRow.plate || selectedMapRow.vin}
</Typography.Title>
<Typography.Text type="tertiary">{selectedMapRow.vin}</Typography.Text>
<div className="vp-selected-vehicle-grid">
<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 spacing={6} wrap>
<Button size="small" 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-integration-panel">
<div className="vp-map-service-queue-title"></div>
{mapIntegrationRows.map((item) => (
@@ -457,7 +491,19 @@ export function Realtime({
mapServiceRows.map((row) => {
const status = vehicleServiceStatus(row);
return (
<div key={`${row.vin}-${row.primaryProtocol}`} className="vp-map-service-item">
<div
key={`${row.vin}-${row.primaryProtocol}`}
role="button"
tabIndex={0}
className={`vp-map-service-item vp-map-service-item-button ${selectedMapRow?.vin === row.vin ? 'vp-map-service-item-selected' : ''}`}
onClick={() => setSelectedMapPointId(realtimeMapPointId(row, rows.indexOf(row)))}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setSelectedMapPointId(realtimeMapPointId(row, rows.indexOf(row)));
}
}}
>
<div className="vp-map-service-item-main">
<div>
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>