feat(platform): add realtime priority queue
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user