feat(platform): add realtime issue checklist

This commit is contained in:
lingniu
2026-07-04 20:09:10 +08:00
parent 7a2a8e8a83
commit bd0b69ddad
2 changed files with 91 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap';
import { getAMapConfig, isAMapConfigured } from '../config/appConfig';
import { buildAppHash } from '../domain/appRoute';
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
function canOpenVehicle(vin?: string) {
@@ -223,6 +224,82 @@ function realtimeOperationsSummaryText({
].join('\n');
}
function appURL(hash: string) {
return `${window.location.origin}${window.location.pathname}${hash}`;
}
function realtimeIssueLabels(row: VehicleRealtimeRow) {
const labels: string[] = [];
const freshness = dataFreshness(row);
if (freshness.stale) {
labels.push(freshness.detail);
}
if (!isValidCoordinate(row)) {
labels.push('坐标无效');
}
if (hasSourceIssue(row)) {
labels.push(row.serviceStatus?.detail || sourceIssueTags(row).join('、') || sourceEvidenceText(row));
}
return labels.length > 0 ? labels : ['暂无异常'];
}
function realtimeIssueAction(row: VehicleRealtimeRow) {
const issues = realtimeIssueLabels(row).join('');
if (!isValidCoordinate(row)) {
return `核对${row.primaryProtocol || '实时来源'}定位字段解析和平台转发`;
}
if (dataFreshness(row).stale) {
return `确认${row.primaryProtocol || '实时来源'}是否持续上报,必要时检查接入链路`;
}
if (hasSourceIssue(row)) {
return '补齐离线来源,确认多来源实时状态一致';
}
return issues === '暂无异常' ? '持续观察' : '结合车辆服务详情继续排查';
}
function realtimeIssueChecklistText({
filters,
rows,
total
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
}) {
const issueRows = rows
.filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row)))
.sort((a, b) => {
const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
if (statusDelta !== 0) return statusDelta;
return Number(dataFreshness(b).stale) - Number(dataFreshness(a).stale);
});
const lines = [
'【实时异常处置清单】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部实时车辆'}`,
`异常车辆:${issueRows.length.toLocaleString()} / 当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`,
''
];
if (issueRows.length === 0) {
lines.push('当前页暂无实时异常车辆。');
}
issueRows.forEach((row, index) => {
const protocol = filters.protocol || row.primaryProtocol || '';
const status = vehicleServiceStatus(row);
const issues = realtimeIssueLabels(row).join('');
lines.push(
`${index + 1}. ${row.plate || '-'} / ${row.vin} / ${protocol || '-'}`,
` 状态:${status.label};在线:${row.online ? '在线' : '离线'};问题:${issues}`,
` 位置:${isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标'};速度:${row.speedKmh ?? '-'} km/hSOC${row.socPercent ?? '-'}%`,
` 最后时间:${row.lastSeen || '-'};建议动作:${realtimeIssueAction(row)}`,
` 车辆服务:${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: 'alert-events', keyword: row.vin, protocol }))}`
);
});
return lines.join('\n');
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
@@ -417,6 +494,7 @@ export function Realtime({
amapConfigured,
sourceIssueRows
}), '实时摘要');
const copyRealtimeIssueChecklist = () => copyText(realtimeIssueChecklistText({ filters, rows, total: pagination.total }), '实时异常处置清单');
const selectRealtimeRow = (row: VehicleRealtimeRow) => {
const index = rows.indexOf(row);
if (index < 0) return;
@@ -538,6 +616,7 @@ export function Realtime({
<Tag color="blue">{locatedCount.toLocaleString()} </Tag>
<Tag color="green">{onlineCount.toLocaleString()} 线</Tag>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时摘要" onClick={copyRealtimeSummary}></Button>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时异常处置清单" onClick={copyRealtimeIssueChecklist}></Button>
</Space>
</div>
<VehicleMap

View File

@@ -8371,6 +8371,18 @@ test('copies realtime operations summary from realtime page', async () => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('重点车辆:'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / YUTONG_MQTT / 车辆离线'));
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('异常车辆3 / 当前页 3 / 总计 3'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / VIN-RT-SUMMARY-003 / JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('状态:车辆离线;在线:离线;问题:'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('坐标无效'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作核对YUTONG_MQTT定位字段解析和平台转发'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务http://localhost:3000/#/detail?keyword=VIN-RT-SUMMARY-003&protocol=JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控http://localhost:3000/#/realtime?keyword=VIN-RT-SUMMARY-003&protocol=JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放http://localhost:3000/#/history?keyword=VIN-RT-SUMMARY-003&protocol=JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警事件http://localhost:3000/#/alert-events?keyword=VIN-RT-SUMMARY-003&protocol=JT808'));
fireEvent.click(screen.getByRole('button', { name: '实时筛选 降级服务 2' }));
expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online');
});