feat(platform): integrate runtime amap vehicle maps

This commit is contained in:
lingniu
2026-07-04 10:52:11 +08:00
parent bb1b2f1c46
commit adb9d146d9
12 changed files with 402 additions and 51 deletions

View File

@@ -3,7 +3,9 @@ import { IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { useEffect, useMemo, useState } from 'react';
import { api, type RawFrameQuery } from '../api/client';
import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap';
import { PageHeader } from '../components/PageHeader';
import { isAMapConfigured } from '../config/appConfig';
type HistoryFilters = {
keyword?: string;
@@ -51,15 +53,6 @@ function hasValidCoordinate(row: HistoryLocationRow) {
return isFiniteNumber(row.longitude) && isFiniteNumber(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}
function mapPointStyle(row: HistoryLocationRow, index: number) {
if (!hasValidCoordinate(row)) {
return { left: `${18 + (index % 4) * 18}%`, top: `${28 + (index % 3) * 17}%` };
}
const left = Math.min(88, Math.max(10, ((row.longitude + 180) / 360) * 100));
const top = Math.min(82, Math.max(12, ((90 - row.latitude) / 180) * 100));
return { left: `${left}%`, top: `${top}%` };
}
function formatNumber(value?: number, suffix = '') {
if (!isFiniteNumber(value)) return '-';
return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}${suffix}`;
@@ -197,7 +190,16 @@ export function History({
const lastLocation = locationItems[locationItems.length - 1];
const currentVehicleKeyword = filters.keyword?.trim() ?? '';
const currentProtocol = filters.protocol?.trim() ?? '';
const amapConfigured = isAMapConfigured();
const selectedFieldCount = splitFields(filters.fields).length;
const playbackPoints: VehicleMapPoint[] = validLocations.map((row, index) => ({
id: `${row.vin}-${row.deviceTime || row.serverTime || index}`,
label: row.plate || row.vin || `${index + 1}`,
longitude: row.longitude,
latitude: row.latitude,
online: true,
title: `${row.deviceTime || row.serverTime || '-'} ${row.speedKmh ?? '-'} km/h`
}));
const filterSummary = [
currentVehicleKeyword ? `车辆:${currentVehicleKeyword}` : '',
currentProtocol ? `数据来源:${currentProtocol}` : '',
@@ -270,18 +272,14 @@ export function History({
<Tag color="blue">{validLocations.length.toLocaleString()} </Tag>
<Tag color={currentProtocol ? 'blue' : 'green'}>{currentProtocol || '全部来源'}</Tag>
<Tag color="green">{formatNumber(mileageDelta, ' km')}</Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图配置就绪' : '高德地图待配置'}</Tag>
</Space>
</div>
<div className="vp-map vp-map-monitor">
{validLocations.slice(0, 120).map((row, index) => (
<span
key={`${row.vin}-${row.deviceTime}-${index}`}
className={`vp-map-dot ${index === 0 ? 'vp-map-dot-start' : index === validLocations.length - 1 ? 'vp-map-dot-end' : 'vp-map-dot-online'}`}
title={`${row.deviceTime} ${row.speedKmh ?? '-'} km/h`}
style={mapPointStyle(row, index)}
/>
))}
</div>
<VehicleMap
points={playbackPoints}
mode="track"
fallbackLabel="高德地图未配置,显示轨迹坐标预览"
/>
</div>
<div className="vp-playback-side">
{[
@@ -316,7 +314,7 @@ export function History({
<Space wrap style={{ marginTop: 12 }}>
<Tag color="grey">{firstLocation?.deviceTime || firstLocation?.serverTime || '-'}</Tag>
<Tag color="grey">{lastLocation?.deviceTime || lastLocation?.serverTime || '-'}</Tag>
<Tag color="grey"> JS API </Tag>
<Tag color="grey"> JS API </Tag>
</Space>
</Card>
<Card bordered style={{ marginTop: 16 }}>

View File

@@ -6,6 +6,8 @@ import { DataEmpty } from '../components/DataEmpty';
import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap';
import { isAMapConfigured } from '../config/appConfig';
function canOpenVehicle(vin?: string) {
const value = vin?.trim();
@@ -36,15 +38,6 @@ function isValidCoordinate(row: VehicleRealtimeRow) {
return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}
function mapPointStyle(row: VehicleRealtimeRow, index: number) {
if (!isValidCoordinate(row)) {
return { left: `${18 + (index % 4) * 18}%`, top: `${28 + (index % 3) * 17}%` };
}
const left = Math.min(88, Math.max(10, ((row.longitude + 180) / 360) * 100));
const top = Math.min(82, Math.max(12, ((90 - row.latitude) / 180) * 100));
return { left: `${left}%`, top: `${top}%` };
}
const onlineLabel: Record<string, string> = {
online: '在线',
offline: '离线'
@@ -70,7 +63,7 @@ export function Realtime({
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
const amapConfigured = Boolean(String(import.meta.env.VITE_AMAP_WEB_JS_KEY ?? '').trim());
const amapConfigured = isAMapConfigured();
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoading(true);
@@ -108,6 +101,14 @@ export function Realtime({
const locatedCount = rows.filter(isValidCoordinate).length;
const degradedCount = rows.filter((row) => row.onlineSourceCount < row.sourceCount).length;
const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean));
const mapPoints: VehicleMapPoint[] = rows.map((row, index) => ({
id: row.vin || `${row.primaryProtocol}-${index}`,
label: row.plate || row.vin || 'unknown',
longitude: row.longitude,
latitude: row.latitude,
online: row.online,
title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
}));
return (
<div className="vp-page">
@@ -161,16 +162,11 @@ export function Realtime({
<Tag color="green">{onlineCount.toLocaleString()} 线</Tag>
</Space>
</div>
<div className="vp-map vp-map-monitor">
{rows.slice(0, 80).map((row, index) => (
<span
key={`${row.vin}-${row.primaryProtocol}-${index}`}
className={`vp-map-dot ${row.online ? 'vp-map-dot-online' : 'vp-map-dot-offline'}`}
title={`${row.plate || row.vin} ${row.primaryProtocol || ''}`}
style={mapPointStyle(row, index)}
/>
))}
</div>
<VehicleMap
points={mapPoints}
maxFallbackPoints={80}
fallbackLabel="高德地图未配置,显示实时坐标预览"
/>
</div>
<div className="vp-monitor-side">
{[
@@ -251,16 +247,7 @@ export function Realtime({
)}
</Tabs.TabPane>
<Tabs.TabPane tab="地图视图" itemKey="map">
<div className="vp-map">
{rows.map((row, index) => (
<span
key={row.vin}
className="vp-map-dot"
title={`${row.plate} ${row.primaryProtocol}`}
style={mapPointStyle(row, index)}
/>
))}
</div>
<VehicleMap points={mapPoints} heightClassName="" fallbackLabel="高德地图未配置,显示实时坐标预览" />
</Tabs.TabPane>
</Tabs>
</Card>