feat(platform): add focus vehicle service card

This commit is contained in:
lingniu
2026-07-04 21:42:04 +08:00
parent 5d57efabea
commit f0f7bc546e
3 changed files with 179 additions and 3 deletions

View File

@@ -48,6 +48,20 @@ type DashboardSnapshotRow = {
detail: string;
};
type FocusVehicleService = {
label: string;
lookupKey: string;
protocol?: string;
reason: string;
statusLabel: string;
statusColor: 'green' | 'orange' | 'red' | 'grey' | 'blue';
realtimeEvidence: string;
historyEvidence: string;
alertEvidence: string;
statisticEvidence: string;
issueType?: string;
};
const dashboardSnapshotColumns: CsvColumn<DashboardSnapshotRow>[] = [
{ title: '模块', value: (row) => row.section },
{ title: '指标', value: (row) => row.item },
@@ -163,6 +177,43 @@ function priorityIssueNotificationText(row: QualityIssueRow) {
].join('\n');
}
function focusVehicleFromIssue(row: QualityIssueRow): FocusVehicleService {
const lookup = qualityIssueVehicleLookup(row);
const filters = priorityIssueEvidenceFilters(row);
const label = priorityIssueVehicleLabel(row);
return {
label,
lookupKey: lookup.key,
protocol: row.protocol,
reason: `${row.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(row.issueType)}`,
statusLabel: row.severity === 'error' ? '优先处置' : '持续跟踪',
statusColor: row.severity === 'error' ? 'red' : 'orange',
realtimeEvidence: `来源 ${qualityProtocolLabel(row.protocol)} / 最后 ${row.lastSeen || '-'}`,
historyEvidence: filters.dateFrom ? `${filters.dateFrom}${filters.dateTo}` : '待确认时间窗',
alertEvidence: row.detail || qualityIssueLabel(row.issueType),
statisticEvidence: '统计查询需回溯车辆口径',
issueType: row.issueType
};
}
function focusVehicleFromRealtime(row: VehicleRealtimeRow): FocusVehicleService {
const status = rowServiceStatus(row);
const label = [row.plate?.trim(), row.vin?.trim()].filter(Boolean).join(' / ') || row.phone || '-';
const location = hasValidCoordinate(row) ? `${row.longitude.toFixed(6)}, ${row.latitude.toFixed(6)}` : '无有效坐标';
return {
label,
lookupKey: row.vin || row.phone || row.plate,
protocol: row.primaryProtocol,
reason: row.online ? '最新在线车辆' : '最新车辆',
statusLabel: status.label,
statusColor: status.color,
realtimeEvidence: `${row.onlineSourceCount}/${row.sourceCount} 来源在线 / ${row.lastSeen || '-'}`,
historyEvidence: location,
alertEvidence: row.onlineSourceCount < row.sourceCount ? '来源不完整,建议检查缺失协议' : '暂无高优先级告警',
statisticEvidence: `速度 ${formatCount(row.speedKmh)} km/h / 里程 ${formatCount(row.totalMileageKm)} km`
};
}
function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record<string, string>) => void) {
const consistency = row.sourceConsistency;
if (!consistency) {
@@ -316,6 +367,7 @@ export function Dashboard({
const highPriorityIssue = qualityIssues.find((item) => item.severity === 'error') ?? qualityIssues[0];
const highPriorityLookup = highPriorityIssue ? qualityIssueVehicleLookup(highPriorityIssue) : undefined;
const highPriorityEvidenceFilters = highPriorityIssue ? priorityIssueEvidenceFilters(highPriorityIssue) : undefined;
const focusVehicle = highPriorityIssue ? focusVehicleFromIssue(highPriorityIssue) : locations[0] ? focusVehicleFromRealtime(locations[0]) : undefined;
const unhealthyLinkCount = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok').length;
const commandMapPoints: VehicleMapPoint[] = locations.map((row, index) => ({
id: row.vin || `${row.primaryProtocol || 'source'}-${index}`,
@@ -602,6 +654,30 @@ export function Dashboard({
];
copyText(lines.join('\n'), '功能蓝图');
};
const copyFocusVehicleService = () => {
if (!focusVehicle) {
Toast.warning('当前没有重点车辆服务可复制');
return;
}
const commonFilters = { keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' };
const lines = [
'【重点车辆服务处置卡】',
`车辆:${focusVehicle.label}`,
`原因:${focusVehicle.reason}`,
`服务状态:${focusVehicle.statusLabel}`,
`实时证据:${focusVehicle.realtimeEvidence}`,
`轨迹证据:${focusVehicle.historyEvidence}`,
`告警证据:${focusVehicle.alertEvidence}`,
`统计证据:${focusVehicle.statisticEvidence}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`RAW证据${appURL(buildAppHash({ page: 'history-query', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol, filters: { ...commonFilters, tab: 'raw', includeFields: 'true' } }))}`,
`统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`告警事件:${appURL(buildAppHash({ page: 'alert-events', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol, filters: focusVehicle.issueType ? { issueType: focusVehicle.issueType } : {} }))}`
];
copyText(lines.join('\n'), '重点车辆服务处置卡');
};
const buildDashboardSnapshotRows = () => {
const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const priorityAction = serviceActionQueue[0];
@@ -738,6 +814,42 @@ export function Dashboard({
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}> CSV</Button>
</Space>
</Card>
{focusVehicle ? (
<Card
bordered
title={<Space><span></span><Tag color={focusVehicle.statusColor}>{focusVehicle.statusLabel}</Tag><Button size="small" onClick={copyFocusVehicleService}></Button></Space>}
style={{ marginBottom: 16 }}
>
<div className="vp-focus-service">
<div className="vp-focus-service-main">
<Tag color="blue">{focusVehicle.reason}</Tag>
<Typography.Title heading={5} style={{ margin: '8px 0 4px' }}>{focusVehicle.label}</Typography.Title>
<Typography.Text type="secondary">RAW</Typography.Text>
</div>
<div className="vp-focus-service-evidence">
{[
{ label: '实时证据', value: focusVehicle.realtimeEvidence },
{ label: '轨迹证据', value: focusVehicle.historyEvidence },
{ label: '告警证据', value: focusVehicle.alertEvidence },
{ label: '统计证据', value: focusVehicle.statisticEvidence }
].map((item) => (
<div key={item.label} className="vp-focus-service-evidence-item">
<span>{item.label}</span>
<strong>{item.value}</strong>
</div>
))}
</div>
<Space wrap>
<Button size="small" theme="solid" type="primary" aria-label="重点车辆 车辆服务" disabled={!focusVehicle.lookupKey} onClick={() => focusVehicle.lookupKey && onOpenVehicle(focusVehicle.lookupKey, focusVehicle.protocol)}></Button>
<Button size="small" theme="light" aria-label="重点车辆 实时监控" disabled={!focusVehicle.lookupKey} onClick={() => onOpenRealtime({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' })}></Button>
<Button size="small" theme="light" aria-label="重点车辆 轨迹回放" disabled={!focusVehicle.lookupKey} onClick={() => onOpenHistory({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' })}></Button>
<Button size="small" theme="light" aria-label="重点车辆 RAW证据" disabled={!focusVehicle.lookupKey} onClick={() => onOpenHistory({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '', tab: 'raw', includeFields: 'true' })}>RAW证据</Button>
<Button size="small" theme="light" aria-label="重点车辆 统计查询" disabled={!focusVehicle.lookupKey} onClick={() => onOpenMileage({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' })}></Button>
<Button size="small" theme="light" type="warning" aria-label="重点车辆 告警事件" onClick={() => onOpenQuality({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '', ...(focusVehicle.issueType ? { issueType: focusVehicle.issueType } : {}) })}></Button>
</Space>
</div>
</Card>
) : null}
<Card bordered title="车辆服务作业台" style={{ marginBottom: 16 }}>
<div className="vp-workbench-grid">
{operationWorkbench.map((item) => (

View File

@@ -1354,6 +1354,52 @@ button.vp-realtime-command-item:focus-visible {
line-height: 30px;
}
.vp-focus-service {
display: grid;
grid-template-columns: minmax(220px, 0.8fr) minmax(420px, 1.6fr) auto;
gap: 16px;
align-items: center;
}
.vp-focus-service-main {
min-width: 0;
}
.vp-focus-service-main .semi-typography {
line-height: 20px;
}
.vp-focus-service-evidence {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.vp-focus-service-evidence-item {
min-height: 78px;
padding: 11px 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 7px;
align-content: start;
}
.vp-focus-service-evidence-item span {
color: var(--vp-text-muted);
font-size: 12px;
line-height: 18px;
}
.vp-focus-service-evidence-item strong {
color: var(--vp-text);
font-size: 13px;
line-height: 19px;
font-weight: 600;
word-break: break-word;
}
.vp-operation-flow {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
@@ -1579,6 +1625,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-source-coverage-grid,
.vp-runbook-grid,
.vp-workbench-grid,
.vp-focus-service,
.vp-focus-service-evidence,
.vp-scenario-grid,
.vp-operation-flow,
.vp-service-model-grid,

View File

@@ -1015,6 +1015,11 @@ test('dashboard exposes end-to-end operations workflow entries', async () => {
test('dashboard surfaces highest priority quality issue with evidence shortcuts', async () => {
window.history.replaceState(null, '', '/#/dashboard');
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText }
});
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/ops/health')) {
@@ -1116,10 +1121,21 @@ test('dashboard surfaces highest priority quality issue with evidence shortcuts'
render(<App />);
expect(await screen.findByText('重点车辆服务')).toBeInTheDocument();
expect(screen.getByText('把协议来源作为证据把实时、轨迹、RAW、统计和告警集中到同一辆车处理。')).toBeInTheDocument();
expect(screen.getByText('实时证据')).toBeInTheDocument();
expect(screen.getAllByText('轨迹证据').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('告警证据')).toBeInTheDocument();
expect(screen.getByText('统计证据')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制处置卡' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【重点车辆服务处置卡】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆粤A告警1 / 13307795425'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控http://localhost:3000/#/realtime?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&includeFields=true'));
expect(await screen.findByText('最高优先级告警')).toBeInTheDocument();
expect(screen.getByText('粤A告警1 / 13307795425')).toBeInTheDocument();
expect(screen.getAllByText('粤A告警1 / 13307795425').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('VIN 缺失')).toBeInTheDocument();
expect(screen.getByText('JT808 数据缺少 VIN 映射')).toBeInTheDocument();
expect(screen.getAllByText('JT808 数据缺少 VIN 映射').length).toBeGreaterThanOrEqual(1);
fireEvent.click(screen.getByRole('button', { name: '首页告警轨迹证据' }));
@@ -2470,7 +2486,7 @@ test('shows row service status in dashboard vehicle previews', async () => {
expect(screen.getAllByText('1/2 来源在线').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('覆盖部分离线')).toBeInTheDocument();
expect(screen.getByText('VIN-REALTIME-OFFLINE')).toBeInTheDocument();
expect(screen.getByText('实时车辆离线')).toBeInTheDocument();
expect(screen.getAllByText('实时车辆离线').length).toBeGreaterThanOrEqual(1);
});
test('filters dashboard coverage from source consistency diagnosis', async () => {