feat(platform): advance vehicle operations center
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user