feat(platform): customerize realtime monitoring view

This commit is contained in:
lingniu
2026-07-05 00:58:16 +08:00
parent 245af2c293
commit b824289cf6
2 changed files with 44 additions and 44 deletions

View File

@@ -34,7 +34,7 @@ function vehicleServiceStatus(row: VehicleRealtimeRow) {
}
function sourceEvidenceText(row: VehicleRealtimeRow) {
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
return `${row.onlineSourceCount}/${row.sourceCount} 通道在线`;
}
function formatPercent(value: number) {
@@ -154,11 +154,11 @@ const realtimeExportColumns: CsvColumn<VehicleRealtimeRow>[] = [
{ title: '车牌', value: (row) => row.plate },
{ title: '手机号', value: (row) => row.phone },
{ title: 'OEM', value: (row) => row.oem },
{ title: '主来源', value: (row) => row.primaryProtocol },
{ title: '来源列表', value: (row) => row.protocols?.join('|') },
{ title: '主通道', value: (row) => row.primaryProtocol },
{ title: '数据通道', value: (row) => row.protocols?.join('|') },
{ title: '在线', value: (row) => row.online ? '在线' : '离线' },
{ title: '车辆服务状态', value: (row) => vehicleServiceStatus(row).label },
{ title: '来源在线', value: (row) => sourceEvidenceText(row) },
{ title: '通道在线', value: (row) => sourceEvidenceText(row) },
{ title: '经度', value: (row) => row.longitude },
{ title: '纬度', value: (row) => row.latitude },
{ title: '速度km/h', value: (row) => row.speedKmh },
@@ -187,7 +187,7 @@ function realtimeExportFileName(filters: Record<string, string>) {
function realtimeFilterSummary(filters: Record<string, string>) {
return [
filters.keyword ? `关键词:${filters.keyword}` : '',
filters.protocol ? `数据来源${filters.protocol}` : '',
filters.protocol ? `数据通道${filters.protocol}` : '',
filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '',
filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : ''
].filter(Boolean);
@@ -265,13 +265,13 @@ function realtimeIssueLabels(row: VehicleRealtimeRow) {
function realtimeIssueAction(row: VehicleRealtimeRow) {
const issues = realtimeIssueLabels(row).join('');
if (!isValidCoordinate(row)) {
return `核对${row.primaryProtocol || '实时来源'}定位字段解析和平台转发`;
return `核对${row.primaryProtocol || '实时数据'}定位字段解析和平台转发`;
}
if (dataFreshness(row).stale) {
return `确认${row.primaryProtocol || '实时来源'}是否持续上报,必要时检查接入链路`;
return `确认${row.primaryProtocol || '实时数据'}是否持续上报,必要时查看告警事件`;
}
if (hasSourceIssue(row)) {
return '补齐离线来源,确认多来源实时状态一致';
return '处理离线数据通道,确认车辆实时状态一致';
}
return issues === '暂无异常' ? '持续观察' : '结合车辆服务详情继续排查';
}
@@ -385,12 +385,12 @@ function realtimeDutyHandoffText({
lines.push(
`${index + 1}. ${row.plate || '-'} / ${row.vin} / ${protocol || '-'} / ${status.label}`,
` 最后上报:${row.lastSeen || '-'};新鲜度:${freshness.label};位置:${isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标'};速度:${row.speedKmh ?? '-'} km/hSOC${row.socPercent ?? '-'}%`,
` 来源${sourceEvidenceText(row)};问题:${realtimeIssueLabels(row).join('')}`,
` 数据覆盖${sourceEvidenceText(row)};问题:${realtimeIssueLabels(row).join('')}`,
` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol }))}`,
` 实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: row.vin, protocol }))}`,
` 轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: row.vin, protocol }))}`,
` 里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: row.vin, protocol }))}`,
` 历史RAW${protocol ? rawFrameAPIURL(row, protocol) : '-'}`
` 原始记录${protocol ? rawFrameAPIURL(row, protocol) : '-'}`
);
});
return lines.join('\n');
@@ -431,7 +431,7 @@ function realtimeImpactReportText({
`定位影响:${locatedCount.toLocaleString()} 辆有有效坐标`,
`服务影响:${degradedCount.toLocaleString()} 辆降级 / ${staleCount.toLocaleString()} 辆超时`,
`地图能力:${amapConfigured ? '高德地图可用' : '高德地图未配置,使用坐标预览'}`,
`建议动作:${impactLevel === '实时稳定' ? '保持监控并关注告警队列' : '优先处理离线、超时和来源不完整车辆,并回到轨迹/RAW证据复核'}`,
`建议动作:${impactLevel === '实时稳定' ? '保持监控并关注告警队列' : '优先处理离线、超时和来源不完整车辆,并回到轨迹和原始记录复核'}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', protocol: filters.protocol, filters }))}`
].join('\n');
}
@@ -728,7 +728,7 @@ export function Realtime({
applyFilters(nextFilters);
}} style={{ marginBottom: 12 }}>
<Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据来源" placeholder="全部来源" style={{ width: 180 }}>
<Form.Select field="protocol" label="数据通道" placeholder="全部数据通道" style={{ width: 180 }}>
<Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
@@ -815,7 +815,7 @@ export function Realtime({
{staleCount > 0 ? <Tag color="red"> {staleCount.toLocaleString()}</Tag> : <Tag color="green"></Tag>}
{sourceIssueRows.length > 0 && onOpenQuality ? (
<Button size="small" theme="light" type="warning" onClick={() => onOpenQuality({ serviceStatus: 'degraded' })}>
{sourceIssueRows.length.toLocaleString()}
{sourceIssueRows.length.toLocaleString()}
</Button>
) : null}
</div>
@@ -825,7 +825,7 @@ export function Realtime({
<div className="vp-current-service-title"></div>
<div className="vp-source-coverage-grid">
{sourceCoverageRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
<Typography.Text type="tertiary"></Typography.Text>
) : sourceCoverageRows.map((item) => (
<button
key={item.protocol}
@@ -894,7 +894,7 @@ export function Realtime({
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).label}</Tag>
<Tag color={selectedMapRow.online ? 'green' : 'orange'}>{selectedMapRow.online ? '在线' : '离线'}</Tag>
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知来源'}</Tag>
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知通道'}</Tag>
</Space>
<Typography.Title heading={6} style={{ margin: '8px 0 0' }}>
{selectedMapRow.plate || selectedMapRow.vin}
@@ -915,7 +915,7 @@ export function Realtime({
</div>
) : null}
<div className="vp-map-integration-panel">
<div className="vp-map-service-queue-title"></div>
<div className="vp-map-service-queue-title"></div>
{mapIntegrationRows.map((item) => (
<div key={item.label} className="vp-map-integration-row">
<div>
@@ -927,9 +927,9 @@ export function Realtime({
))}
</div>
<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 ? (
<Typography.Text type="tertiary"></Typography.Text>
<Typography.Text type="tertiary"></Typography.Text>
) : (
sourceIssueRows.map((row) => {
const status = vehicleServiceStatus(row);
@@ -946,16 +946,16 @@ export function Realtime({
<Tag color="blue">{sourceEvidenceText(row)}</Tag>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
{sourceIssueTags(row).map((item) => (
<Tag key={item} color="orange">{item}</Tag>
<Tag key={item} color="orange">{item}</Tag>
))}
</Space>
<Typography.Text type="secondary" size="small">
{row.serviceStatus?.detail || sourceEvidenceText(row)}
{row.serviceStatus?.detail || sourceEvidenceText(row)}
</Typography.Text>
<div className="vp-map-service-item-footer">
<Typography.Text type="tertiary" size="small">{row.lastSeen || '-'}</Typography.Text>
<Space spacing={4} wrap>
<Button size="small" disabled={!onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button size="small" disabled={!onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button size="small" onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
</Space>
</div>
@@ -965,7 +965,7 @@ export function Realtime({
)}
</div>
<div className="vp-map-service-queue">
<div className="vp-map-service-queue-title"></div>
<div className="vp-map-service-queue-title"></div>
{mapServiceRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : (
@@ -1006,8 +1006,8 @@ export function Realtime({
<Space spacing={4} wrap>
<Button size="small" onClick={() => window.open(amapMarkerURL(row), '_blank', 'noopener,noreferrer')}></Button>
<Button size="small" onClick={() => onOpenHistory?.(row.vin, filters.protocol || row.primaryProtocol)}></Button>
<Button size="small" disabled={!onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button size="small" onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
<Button size="small" disabled={!onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button size="small" onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
</Space>
</div>
</div>
@@ -1117,13 +1117,13 @@ export function Realtime({
)
},
{
title: '来源证据',
title: '实时通道',
width: 260,
render: (_: unknown, row: VehicleRealtimeRow) => (
<SourceStatusTags sourceStatus={row.sourceStatus} protocols={row.protocols} lastSeen={row.lastSeen} />
)
},
{ title: '据覆盖', width: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) },
{ title: '据覆盖', width: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) },
{ title: '在线', width: 90, render: (_: unknown, row: VehicleRealtimeRow) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
{ title: '最后时间', dataIndex: 'lastSeen', width: 170 },
{
@@ -1133,7 +1133,7 @@ export function Realtime({
<Space spacing={4} wrap>
<Button disabled={!isValidCoordinate(row)} onClick={() => selectRealtimeRow(row)}></Button>
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
<Button disabled={!canOpenVehicle(row.vin) || !onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button disabled={!canOpenVehicle(row.vin) || !onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
</Space>
)
}

View File

@@ -192,7 +192,7 @@ test('exposes AMap operations shortcuts when map key is configured', async () =>
fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' }));
expect(window.location.hash).toBe('#/map');
expect(await screen.findByRole('heading', { name: '实时地图' })).toBeInTheDocument();
expect(screen.getByText('地图车辆服务队列')).toBeInTheDocument();
expect(screen.getByText('地图车辆队列')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' }));
expect(window.location.hash).toBe('#/realtime');
fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' }));
@@ -8791,11 +8791,11 @@ test('frames realtime page as one vehicle service with source evidence', async (
expect(screen.getByText('高德地图待配置')).toBeInTheDocument();
expect(screen.getAllByText('定位有效').length).toBeGreaterThan(0);
expect(screen.getByText('车辆核心数据')).toBeInTheDocument();
expect(screen.getByText('来源证据')).toBeInTheDocument();
expect(screen.getByText('实时通道')).toBeInTheDocument();
expect(screen.getByText('GB32960 在线')).toBeInTheDocument();
expect(screen.getByText('JT808 在线')).toBeInTheDocument();
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0);
expect(screen.getAllByText('2/2 通道在线').length).toBeGreaterThan(0);
expect(screen.getByText('实时运营影响')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制影响报告/ })).toBeInTheDocument();
expect(screen.getByText('需要处置')).toBeInTheDocument();
@@ -9177,7 +9177,7 @@ test('copies realtime operations summary from realtime page', async () => {
fireEvent.click(screen.getByRole('button', { name: '复制实时摘要' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时监控摘要】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据来源JT808在线在线'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据通道JT808在线在线'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆总数3当前页3'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆2定位有效2'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('需要关注2数据通道3'));
@@ -9187,7 +9187,7 @@ test('copies realtime operations summary from realtime page', async () => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时页面http://localhost:3000/#/realtime?protocol=JT808&online=online'));
fireEvent.click(screen.getByRole('button', { name: '复制实时异常处置清单' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时异常处置清单】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据来源JT808在线在线'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据通道JT808在线在线'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常车辆3 / 当前页 3 / 总计 3'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / VIN-RT-SUMMARY-003 / JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('状态:车辆离线;在线:离线;问题:'));
@@ -9205,7 +9205,7 @@ 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/#/detail?keyword=VIN-RT-SUMMARY-003&protocol=JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程统计http://localhost:3000/#/mileage?keyword=VIN-RT-SUMMARY-003&protocol=JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史RAWhttp://localhost:3000/api/history/raw-frames?protocol=JT808&vin=VIN-RT-SUMMARY-003&limit=20&includeFields=true'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录http://localhost:3000/api/history/raw-frames?protocol=JT808&vin=VIN-RT-SUMMARY-003&limit=20&includeFields=true'));
fireEvent.click(screen.getByRole('button', { name: '实时筛选 需要关注 2' }));
expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online');
fireEvent.click(screen.getByRole('button', { name: '筛选数据通道 GB32960' }));
@@ -9313,7 +9313,7 @@ test('shows production AMap integration status on realtime page', async () => {
render(<App />);
expect(await screen.findByText('地图能力状态')).toBeInTheDocument();
expect(await screen.findByText('地图展示状态')).toBeInTheDocument();
expect(screen.getByText('Web JS Key')).toBeInTheDocument();
expect(screen.getByText('服务端 API Key')).toBeInTheDocument();
expect(screen.getAllByText('已配置').length).toBeGreaterThan(0);
@@ -9508,10 +9508,10 @@ test('opens vehicle service from realtime map service queue', async () => {
render(<App />);
expect(await screen.findByText('地图车辆服务队列')).toBeInTheDocument();
expect(await screen.findByText('地图车辆队列')).toBeInTheDocument();
expect(screen.getAllByText('粤AMAP01').length).toBeGreaterThan(0);
expect(screen.getByText('JT808 离线GB32960 仍可支撑车辆服务')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '地图车辆服务' }));
fireEvent.click(screen.getByRole('button', { name: '车辆详情' }));
await waitFor(() => {
expect(window.location.hash).toBe('#/detail?keyword=VIN-MAP-001&protocol=GB32960');
@@ -9579,7 +9579,7 @@ test('opens amap coordinate and trajectory playback from realtime map service qu
render(<App />);
expect(await screen.findByText('地图车辆服务队列')).toBeInTheDocument();
expect(await screen.findByText('地图车辆队列')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '高德坐标' }));
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining('https://uri.amap.com/marker?position=113.24567,23.12345'),
@@ -9682,11 +9682,11 @@ test('surfaces multi-source realtime consistency issues with quality drilldown',
render(<App />);
expect(await screen.findByText('车辆数据一致性检查')).toBeInTheDocument();
expect(await screen.findByText('需关注车辆')).toBeInTheDocument();
expect(screen.getAllByText('粤A一致监控').length).toBeGreaterThan(0);
expect(screen.getByText('一致性诊断YUTONG_MQTT 离线GB32960/JT808 仍可支撑车辆服务')).toBeInTheDocument();
expect(screen.getByText('一致性YUTONG_MQTT 离线')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '检查质量' }));
expect(screen.getByText('处置说明YUTONG_MQTT 离线GB32960/JT808 仍可支撑车辆服务')).toBeInTheDocument();
expect(screen.getByText('问题YUTONG_MQTT 离线')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '查看告警' }));
await waitFor(() => {
expect(window.location.hash).toBe('#/alert-events?keyword=VIN-RT-CONSISTENCY&protocol=GB32960');
@@ -9816,7 +9816,7 @@ test('opens quality issues from realtime vehicle row with source evidence', asyn
render(<App />);
expect((await screen.findAllByText('VIN-RT-QUALITY')).length).toBeGreaterThan(0);
fireEvent.click(screen.getAllByRole('button', { name: '质量问题' })[0]);
fireEvent.click(screen.getAllByRole('button', { name: '告警事件' })[0]);
await waitFor(() => {
expect(window.location.hash).toBe('#/alert-events?keyword=VIN-RT-QUALITY&protocol=JT808');
@@ -9991,7 +9991,7 @@ test('shows and clears current realtime service filters', async () => {
expect(await screen.findByText('当前实时筛选')).toBeInTheDocument();
expect(screen.getByText('关键词粤ART002')).toBeInTheDocument();
expect(screen.getByText('数据来源JT808')).toBeInTheDocument();
expect(screen.getByText('数据通道JT808')).toBeInTheDocument();
expect(screen.getByText('在线:在线')).toBeInTheDocument();
expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '清空筛选' }));
@@ -10030,7 +10030,7 @@ test('updates realtime hash when source filters are submitted', async () => {
await screen.findByRole('heading', { name: '实时监控' });
fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号 / OEM'), { target: { value: '粤ART002' } });
fireEvent.click(screen.getByText('全部来源'));
fireEvent.click(screen.getByText('全部数据通道'));
fireEvent.click(await screen.findByText('JT808'));
fireEvent.click(screen.getByRole('button', { name: '查询' }));