feat(platform): add realtime priority queue

This commit is contained in:
lingniu
2026-07-05 21:22:04 +08:00
parent 82faa046b7
commit 059e4586a7
3 changed files with 203 additions and 0 deletions

View File

@@ -990,6 +990,10 @@ export function Realtime({
{ label: '有效定位', value: locatedCount.toLocaleString(), color: locatedCount > 0 ? 'green' as const : 'orange' as const, helper: `定位率 ${formatPercent(locatedRate)}` },
{ label: '需关注', value: mapAttentionRows.length.toLocaleString(), color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, helper: `${staleCount.toLocaleString()} 辆更新超时` }
];
const realtimePriorityRows = mapAttentionRows.slice(0, 4);
const realtimePrioritySummary = realtimePriorityRows.length > 0
? `${realtimePriorityRows.length.toLocaleString()} 辆优先处置`
: '暂无阻断';
const missingCoordinateCount = Math.max(0, rows.length - locatedCount);
const selectedVehicleProtocol = selectedMapRow ? filters.protocol || selectedMapRow.primaryProtocol || '' : '';
const selectedVehicleLabel = selectedMapRow?.plate || selectedMapRow?.vin || '未选择车辆';
@@ -2210,6 +2214,54 @@ export function Realtime({
))}
</div>
</section>
<section className="vp-realtime-priority-board" aria-label="实时处置优先级">
<div className="vp-realtime-priority-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={realtimePriorityRows.length > 0 ? 'orange' : 'green'}>{realtimePrioritySummary}</Tag>
<Tag color="grey"> {rows.length.toLocaleString()} </Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-realtime-priority-grid">
{realtimePriorityRows.length === 0 ? (
<div className="vp-realtime-priority-empty">
<Tag color="green"></Tag>
<strong></strong>
<span></span>
</div>
) : realtimePriorityRows.map((row, index) => {
const label = row.plate || row.vin || '未知车辆';
const protocol = filters.protocol || row.primaryProtocol || '';
const issueText = realtimeIssueLabels(row).join('');
return (
<article key={`${row.vin}-${row.primaryProtocol}-realtime-priority`} className="vp-realtime-priority-item">
<div className="vp-realtime-priority-rank">{index + 1}</div>
<div className="vp-realtime-priority-main">
<Space wrap>
<Tag color={vehicleServiceStatus(row).color}>{vehicleServiceStatus(row).label}</Tag>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
<Tag color={isValidCoordinate(row) ? 'green' : 'orange'}>{isValidCoordinate(row) ? '有定位' : '无定位'}</Tag>
</Space>
<strong>{label}</strong>
<span>{issueText}</span>
<small>{row.vin} / {protocol || '全部数据通道'} / {row.lastSeen || '-'}</small>
</div>
<div className="vp-realtime-priority-actions">
<button type="button" onClick={() => onOpenQuality?.({ keyword: row.vin, protocol })} disabled={!onOpenQuality || !canOpenVehicle(row.vin)} aria-label={`实时处置优先级 ${index + 1} ${label} 查看告警`}></button>
<button type="button" onClick={() => onOpenHistory?.(row.vin, protocol)} disabled={!canOpenVehicle(row.vin)} aria-label={`实时处置优先级 ${index + 1} ${label} 轨迹回放`}></button>
<button type="button" onClick={() => onOpenVehicle(row.vin, protocol)} disabled={!canOpenVehicle(row.vin)} aria-label={`实时处置优先级 ${index + 1} ${label} 车辆档案`}></button>
</div>
</article>
);
})}
</div>
</section>
<Card bordered className="vp-realtime-customer-board" bodyStyle={{ padding: 0 }}>
<div className="vp-realtime-customer-summary">
<Space wrap>

View File

@@ -2732,6 +2732,138 @@ body {
line-height: 18px;
}
.vp-realtime-priority-board {
margin-bottom: 16px;
border: 1px solid rgba(255, 122, 69, 0.22);
border-radius: var(--vp-radius);
background: #fff;
display: grid;
grid-template-columns: minmax(300px, 0.48fr) minmax(0, 1.52fr);
overflow: hidden;
box-shadow: var(--vp-shadow-sm);
}
.vp-realtime-priority-summary {
padding: 18px;
border-right: 1px solid var(--vp-border);
background: #fff8f3;
display: grid;
gap: 12px;
align-content: start;
}
.vp-realtime-priority-summary .semi-typography-secondary {
font-size: 13px;
line-height: 20px;
}
.vp-realtime-priority-grid {
min-width: 0;
padding: 14px;
display: grid;
gap: 10px;
}
.vp-realtime-priority-item {
min-width: 0;
padding: 12px;
border: 1px solid rgba(255, 122, 69, 0.22);
border-radius: 8px;
background: #fffdfb;
display: grid;
grid-template-columns: 36px minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
}
.vp-realtime-priority-rank {
width: 32px;
height: 32px;
border-radius: 8px;
background: #fff1e8;
color: #d25f00;
font-size: 15px;
font-weight: 800;
line-height: 32px;
text-align: center;
}
.vp-realtime-priority-main {
min-width: 0;
display: grid;
gap: 6px;
}
.vp-realtime-priority-main strong {
color: var(--vp-text);
font-size: 17px;
line-height: 23px;
word-break: break-word;
}
.vp-realtime-priority-main span,
.vp-realtime-priority-main small {
color: var(--vp-text-muted);
font-size: 12px;
line-height: 18px;
word-break: break-word;
}
.vp-realtime-priority-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.vp-realtime-priority-actions button {
min-height: 30px;
padding: 0 10px;
border: 1px solid rgba(22, 100, 255, 0.22);
border-radius: 8px;
background: #f8fbff;
color: var(--vp-primary);
cursor: pointer;
font: inherit;
font-size: 12px;
font-weight: 700;
}
.vp-realtime-priority-actions button:hover,
.vp-realtime-priority-actions button:focus-visible {
border-color: rgba(22, 100, 255, 0.45);
background: #eef5ff;
outline: none;
}
.vp-realtime-priority-actions button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.vp-realtime-priority-empty {
min-height: 118px;
padding: 14px;
border: 1px solid rgba(67, 160, 71, 0.2);
border-radius: 8px;
background: #f8fff8;
display: grid;
gap: 8px;
align-content: center;
}
.vp-realtime-priority-empty strong {
color: var(--vp-text);
font-size: 17px;
line-height: 24px;
}
.vp-realtime-priority-empty span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-realtime-customer-board {
margin-bottom: 16px;
overflow: hidden;
@@ -10159,6 +10291,7 @@ button.vp-realtime-command-item:focus-visible {
.vp-time-delivery-grid,
.vp-realtime-command-center,
.vp-realtime-command-center-grid,
.vp-realtime-priority-board,
.vp-realtime-customer-board .semi-card-body,
.vp-realtime-question-board,
.vp-realtime-question-grid,
@@ -10357,6 +10490,18 @@ button.vp-realtime-command-item:focus-visible {
grid-template-columns: 1fr;
}
.vp-realtime-priority-item {
grid-template-columns: 1fr;
}
.vp-realtime-priority-rank {
width: 100%;
}
.vp-realtime-priority-actions {
justify-content: flex-start;
}
.vp-map-commandbar,
.vp-mileage-console-head,
.vp-map-canvas-toolbar {

View File

@@ -9878,6 +9878,12 @@ test('realtime page prioritizes customer vehicle monitoring command center', asy
expect(screen.getByRole('button', { name: '客户实时监控总控台 轨迹复盘 粤A实时1 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户实时监控总控台 导出交付 2 当前页 导出CSV' })).toBeInTheDocument();
expect(screen.getByText('来源证据仅用于可信度判断JT808 / GB32960')).toBeInTheDocument();
expect(screen.getByText('实时处置优先级')).toBeInTheDocument();
expect(screen.getByText('按客户影响排序:先处理不能看车、定位无效、数据通道不完整和更新超时的车辆。')).toBeInTheDocument();
expect(screen.getAllByText('粤A实时2').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText((content) => content.includes('坐标无效')).length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText((content) => content.includes('缺少 GB32960')).length).toBeGreaterThanOrEqual(1);
expect(screen.getByRole('button', { name: '实时处置优先级 1 粤A实时2 查看告警' })).toBeInTheDocument();
});
test('frames realtime page as one vehicle service with source evidence', async () => {