feat(platform): add realtime issue checklist
This commit is contained in:
@@ -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/h;SOC:${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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user