feat(platform): show realtime amap integration status

This commit is contained in:
lingniu
2026-07-04 15:17:14 +08:00
parent 46024151a0
commit 4742361c7f
3 changed files with 165 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags'; import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag'; import { StatusTag } from '../components/StatusTag';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap'; 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'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
function canOpenVehicle(vin?: string) { function canOpenVehicle(vin?: string) {
@@ -190,7 +190,8 @@ export function Realtime({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>(initialFilters); const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 }); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
const amapConfigured = isAMapConfigured(); const amapConfig = getAMapConfig();
const amapConfigured = isAMapConfigured(amapConfig);
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoading(true); setLoading(true);
@@ -266,6 +267,32 @@ export function Realtime({
online: row.online, online: row.online,
title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` 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({ const copyRealtimeSummary = () => copyText(realtimeOperationsSummaryText({
filters, filters,
rows, rows,
@@ -353,6 +380,18 @@ export function Realtime({
<Typography.Text type="secondary"> <Typography.Text type="secondary">
Web JS Key Key Web JS Key Key
</Typography.Text> </Typography.Text>
<div className="vp-map-integration-panel">
<div className="vp-map-service-queue-title"></div>
{mapIntegrationRows.map((item) => (
<div key={item.label} className="vp-map-integration-row">
<div>
<Typography.Text strong>{item.label}</Typography.Text>
<Typography.Text type="tertiary" size="small">{item.detail}</Typography.Text>
</div>
<Tag color={item.color}>{item.value}</Tag>
</div>
))}
</div>
<div className="vp-source-consistency-panel"> <div className="vp-source-consistency-panel">
<div className="vp-map-service-queue-title"></div> <div className="vp-map-service-queue-title"></div>
{sourceIssueRows.length === 0 ? ( {sourceIssueRows.length === 0 ? (

View File

@@ -393,6 +393,35 @@ body {
border-top: 1px solid var(--vp-border); 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 { .vp-map-service-queue-title {
color: var(--vp-text); color: var(--vp-text);
font-size: 13px; font-size: 13px;

View File

@@ -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')); 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(<App />);
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 () => { test('opens vehicle service from realtime map service queue', async () => {
window.history.replaceState(null, '', '/#/realtime'); window.history.replaceState(null, '', '/#/realtime');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {