feat(platform): show realtime amap integration status
This commit is contained in:
@@ -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<Record<string, string>>(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<string, string> = 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({
|
||||
<Typography.Text type="secondary">
|
||||
高德 Web JS Key 和安全密钥通过运行环境配置,前端只读取公开 Key;安全密钥后续走服务端代理,避免明文下发。
|
||||
</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-map-service-queue-title">多来源一致性检查</div>
|
||||
{sourceIssueRows.length === 0 ? (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(<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 () => {
|
||||
window.history.replaceState(null, '', '/#/realtime');
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
|
||||
Reference in New Issue
Block a user