feat(platform): add time window service audit
This commit is contained in:
@@ -125,6 +125,16 @@ function previousDateString(value: string) {
|
|||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dayRangeText(dateFrom?: string, dateTo?: string) {
|
||||||
|
if (!dateFrom && !dateTo) return '未限定时间';
|
||||||
|
if (!dateFrom || !dateTo) return '单边时间窗';
|
||||||
|
const start = new Date(`${dateFrom}T00:00:00+08:00`).getTime();
|
||||||
|
const end = new Date(`${dateTo}T00:00:00+08:00`).getTime();
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return '时间格式待确认';
|
||||||
|
const days = Math.max(0, Math.round((end - start) / 86400000));
|
||||||
|
return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`;
|
||||||
|
}
|
||||||
|
|
||||||
function priorityIssueVehicleLabel(row: QualityIssueRow) {
|
function priorityIssueVehicleLabel(row: QualityIssueRow) {
|
||||||
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
|
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
|
||||||
return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-';
|
return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-';
|
||||||
@@ -806,12 +816,41 @@ export function Dashboard({
|
|||||||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const
|
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
const timeMonitorScopeValue = timeMonitorScope();
|
||||||
|
const timeMonitorWindowLabel = dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo);
|
||||||
|
const timeMonitorChecklist = [
|
||||||
|
{
|
||||||
|
label: '范围可解释',
|
||||||
|
value: timeMonitorScopeValue.keyword ? '单车聚焦' : '全域巡检',
|
||||||
|
detail: `${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorWindowLabel}`,
|
||||||
|
color: timeMonitorScopeValue.dateFrom && timeMonitorScopeValue.dateTo ? 'green' as const : 'orange' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '轨迹可复盘',
|
||||||
|
value: `${formatCount(summary?.activeToday)} 活跃车辆`,
|
||||||
|
detail: '可进入轨迹页核对位置、速度、里程断点和停驶片段。',
|
||||||
|
color: (summary?.activeToday ?? 0) > 0 ? 'green' as const : 'orange' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据可导出',
|
||||||
|
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||||||
|
detail: '历史查询默认带 RAW 与解析字段,支持按字段缩小导出体积。',
|
||||||
|
color: (summary?.frameToday ?? 0) > 0 ? 'blue' as const : 'orange' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '异常可闭环',
|
||||||
|
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||||||
|
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有高优先级告警。',
|
||||||
|
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const
|
||||||
|
}
|
||||||
|
];
|
||||||
const copyTimeMonitorReviewPackage = () => {
|
const copyTimeMonitorReviewPackage = () => {
|
||||||
const scope = timeMonitorScope();
|
const scope = timeMonitorScope();
|
||||||
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
|
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
|
||||||
const lines = [
|
const lines = [
|
||||||
'【客户时间窗复盘包】',
|
'【客户时间窗复盘包】',
|
||||||
`范围:${timeMonitorSummary}`,
|
`范围:${timeMonitorSummary}`,
|
||||||
|
`时间窗判定:${dayRangeText(scope.dateFrom, scope.dateTo)}`,
|
||||||
'平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知',
|
'平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知',
|
||||||
`车辆范围:${scope.keyword || '全部车辆'}`,
|
`车辆范围:${scope.keyword || '全部车辆'}`,
|
||||||
`数据通道:${scope.protocol || '全部数据通道'}`,
|
`数据通道:${scope.protocol || '全部数据通道'}`,
|
||||||
@@ -820,6 +859,8 @@ export function Dashboard({
|
|||||||
`统计证据:车辆口径 ${formatCount(serviceSummary?.totalVehicles)},用于区间里程和日统计闭合`,
|
`统计证据:车辆口径 ${formatCount(serviceSummary?.totalVehicles)},用于区间里程和日统计闭合`,
|
||||||
`历史证据:今日数据 ${formatCount(summary?.frameToday)},用于 RAW 与解析字段复核`,
|
`历史证据:今日数据 ${formatCount(summary?.frameToday)},用于 RAW 与解析字段复核`,
|
||||||
`告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`,
|
`告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`,
|
||||||
|
'交付核对:',
|
||||||
|
...timeMonitorChecklist.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`),
|
||||||
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
|
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
|
||||||
`里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
|
`里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
|
||||||
`历史数据查询:${appURL(buildAppHash({ page: 'history-query', keyword: scope.keyword, protocol: scope.protocol, filters: rawFilters }))}`,
|
`历史数据查询:${appURL(buildAppHash({ page: 'history-query', keyword: scope.keyword, protocol: scope.protocol, filters: rawFilters }))}`,
|
||||||
@@ -1390,6 +1431,23 @@ export function Dashboard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="vp-time-audit-strip">
|
||||||
|
<div className="vp-time-audit-head">
|
||||||
|
<Typography.Text strong>时间窗服务核对</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
交付给客户前,先确认范围、轨迹、导出和异常闭环四件事都能讲清楚。
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="vp-time-audit-grid">
|
||||||
|
{timeMonitorChecklist.map((item) => (
|
||||||
|
<div key={item.label} className="vp-time-audit-item">
|
||||||
|
<Tag color={item.color}>{item.label}</Tag>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
<span>{item.detail}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="vp-time-monitor-workbench">
|
<div className="vp-time-monitor-workbench">
|
||||||
<div className="vp-time-monitor-scope">
|
<div className="vp-time-monitor-scope">
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
|||||||
@@ -580,6 +580,59 @@ body {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-strip {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(22, 100, 255, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcff;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 0.56fr) minmax(0, 1.44fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-head {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-head .semi-typography {
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-item {
|
||||||
|
min-height: 122px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-item strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-time-audit-item span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-time-monitor-workbench {
|
.vp-time-monitor-workbench {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -3971,6 +4024,8 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-vehicle-decision-grid,
|
.vp-vehicle-decision-grid,
|
||||||
.vp-time-review-package,
|
.vp-time-review-package,
|
||||||
.vp-time-review-grid,
|
.vp-time-review-grid,
|
||||||
|
.vp-time-audit-strip,
|
||||||
|
.vp-time-audit-grid,
|
||||||
.vp-realtime-command-board,
|
.vp-realtime-command-board,
|
||||||
.vp-realtime-command-grid,
|
.vp-realtime-command-grid,
|
||||||
.vp-realtime-impact-board,
|
.vp-realtime-impact-board,
|
||||||
|
|||||||
@@ -818,6 +818,12 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
|||||||
expect(screen.getByText('车辆范围')).toBeInTheDocument();
|
expect(screen.getByText('车辆范围')).toBeInTheDocument();
|
||||||
expect(screen.getByText('复盘时间')).toBeInTheDocument();
|
expect(screen.getByText('复盘时间')).toBeInTheDocument();
|
||||||
expect(screen.getByText('统计证据')).toBeInTheDocument();
|
expect(screen.getByText('统计证据')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('时间窗服务核对')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('交付给客户前,先确认范围、轨迹、导出和异常闭环四件事都能讲清楚。')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('范围可解释')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('轨迹可复盘')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('数据可导出')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('异常可闭环')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '复制复盘包' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: '复制复盘包' })).toBeInTheDocument();
|
||||||
expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument();
|
expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument();
|
||||||
expect(screen.getByText('查看轨迹')).toBeInTheDocument();
|
expect(screen.getByText('查看轨迹')).toBeInTheDocument();
|
||||||
@@ -928,6 +934,9 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '复制复盘包' }));
|
fireEvent.click(screen.getByRole('button', { name: '复制复盘包' }));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台能力'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台能力'));
|
||||||
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('时间窗判定:1 天窗口'));
|
||||||
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付核对:'));
|
||||||
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 范围可解释'));
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
await renderDashboard();
|
await renderDashboard();
|
||||||
|
|||||||
Reference in New Issue
Block a user