diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index daccaae4..665d16ed 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -8,7 +8,7 @@ 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'; +import { getAMapConfig, isAMapConfigured } from '../config/appConfig'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; function canOpenVehicle(vin?: string) { @@ -190,7 +190,8 @@ export function Realtime({ const [loading, setLoading] = useState(true); const [filters, setFilters] = useState>(initialFilters); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 }); - const amapConfigured = isAMapConfigured(); + const amapConfig = getAMapConfig(); + const amapConfigured = isAMapConfigured(amapConfig); const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoading(true); @@ -266,6 +267,32 @@ export function Realtime({ online: row.online, title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` })); + const mapIntegrationRows = [ + { + label: 'Web JS Key', + value: amapConfigured ? '已配置' : '未配置', + color: amapConfigured ? 'green' as const : 'orange' as const, + detail: amapConfigured ? '前端可加载高德 Web JS API。' : '缺少公开 Key,地图将使用坐标预览。' + }, + { + label: '安全代理', + value: amapConfig.securityServiceHost || '未启用', + color: amapConfig.securityServiceHost ? 'green' as const : 'grey' as const, + detail: amapConfig.securityServiceHost ? '安全密钥由服务端追加,不下发到浏览器。' : '未配置代理时会退回前端安全码模式。' + }, + { + label: '定位覆盖', + value: `${locatedCount.toLocaleString()} / ${rows.length.toLocaleString()}`, + color: locatedCount > 0 ? 'blue' as const : 'orange' as const, + detail: '当前页有效经纬度车辆数。' + }, + { + label: '高德 URI', + value: '坐标跳转', + color: 'blue' as const, + detail: '车辆队列可直接打开高德坐标,轨迹页可打开线路。' + } + ]; const copyRealtimeSummary = () => copyText(realtimeOperationsSummaryText({ filters, rows, @@ -353,6 +380,18 @@ export function Realtime({ 高德 Web JS Key 和安全密钥通过运行环境配置,前端只读取公开 Key;安全密钥后续走服务端代理,避免明文下发。 +
+
地图接入状态
+ {mapIntegrationRows.map((item) => ( +
+
+ {item.label} + {item.detail} +
+ {item.value} +
+ ))} +
多来源一致性检查
{sourceIssueRows.length === 0 ? ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 1487bef0..99506395 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -393,6 +393,35 @@ body { border-top: 1px solid var(--vp-border); } +.vp-map-integration-panel { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fff; +} + +.vp-map-integration-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + padding-bottom: 8px; + border-bottom: 1px solid var(--vp-border); +} + +.vp-map-integration-row:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.vp-map-integration-row > div { + display: grid; + gap: 2px; + min-width: 0; +} + .vp-map-service-queue-title { color: var(--vp-text); font-size: 13px; diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 78606286..3acd4038 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -7405,6 +7405,101 @@ test('copies realtime operations summary from realtime page', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时页面:http://localhost:3000/#/realtime?protocol=JT808&online=online')); }); +test('shows production AMap integration status on realtime page', async () => { + window.history.replaceState(null, '', '/#/realtime'); + Object.defineProperty(window, '__LINGNIU_APP_CONFIG__', { + configurable: true, + value: { amapWebJsKey: 'amap-key', amapSecurityServiceHost: '/_AMapService' } + }); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/realtime/vehicles')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { + vin: 'VIN-AMAP-READY-001', + plate: '粤A地图就绪', + phone: '13300000001', + oem: 'G7s', + protocols: ['JT808'], + sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], + sourceCount: 1, + onlineSourceCount: 1, + online: true, + bindingStatus: 'bound', + primaryProtocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 32, + socPercent: 76, + totalMileageKm: 1200.5, + lastSeen: '2026-07-03 20:12:10', + serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } + }, + { + vin: 'VIN-AMAP-READY-002', + plate: '粤A地图无坐标', + phone: '13300000002', + oem: 'G7s', + protocols: ['GB32960'], + sourceStatus: [{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], + sourceCount: 1, + onlineSourceCount: 1, + online: true, + bindingStatus: 'bound', + primaryProtocol: 'GB32960', + longitude: 0, + latitude: 0, + speedKmh: 12, + socPercent: 68, + totalMileageKm: 2200.5, + lastSeen: '2026-07-03 20:12:08', + serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } + } + ], + total: 2, + limit: 50, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 50, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('地图接入状态')).toBeInTheDocument(); + expect(screen.getByText('Web JS Key')).toBeInTheDocument(); + expect(screen.getAllByText('已配置').length).toBeGreaterThan(0); + expect(screen.getByText('安全代理')).toBeInTheDocument(); + expect(screen.getByText('/_AMapService')).toBeInTheDocument(); + expect(screen.getByText('定位覆盖')).toBeInTheDocument(); + expect(screen.getByText('1 / 2')).toBeInTheDocument(); +}); + test('opens vehicle service from realtime map service queue', async () => { window.history.replaceState(null, '', '/#/realtime'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {