feat(platform): add time window service audit
This commit is contained in:
@@ -125,6 +125,16 @@ function previousDateString(value: string) {
|
||||
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) {
|
||||
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
|
||||
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
|
||||
}
|
||||
];
|
||||
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 scope = timeMonitorScope();
|
||||
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
|
||||
const lines = [
|
||||
'【客户时间窗复盘包】',
|
||||
`范围:${timeMonitorSummary}`,
|
||||
`时间窗判定:${dayRangeText(scope.dateFrom, scope.dateTo)}`,
|
||||
'平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知',
|
||||
`车辆范围:${scope.keyword || '全部车辆'}`,
|
||||
`数据通道:${scope.protocol || '全部数据通道'}`,
|
||||
@@ -820,6 +859,8 @@ export function Dashboard({
|
||||
`统计证据:车辆口径 ${formatCount(serviceSummary?.totalVehicles)},用于区间里程和日统计闭合`,
|
||||
`历史证据:今日数据 ${formatCount(summary?.frameToday)},用于 RAW 与解析字段复核`,
|
||||
`告警事件:${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: 'mileage', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
|
||||
`历史数据查询:${appURL(buildAppHash({ page: 'history-query', keyword: scope.keyword, protocol: scope.protocol, filters: rawFilters }))}`,
|
||||
@@ -1390,6 +1431,23 @@ export function Dashboard({
|
||||
))}
|
||||
</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-scope">
|
||||
<Space wrap>
|
||||
|
||||
@@ -580,6 +580,59 @@ body {
|
||||
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 {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
@@ -3971,6 +4024,8 @@ button.vp-realtime-command-item:focus-visible {
|
||||
.vp-vehicle-decision-grid,
|
||||
.vp-time-review-package,
|
||||
.vp-time-review-grid,
|
||||
.vp-time-audit-strip,
|
||||
.vp-time-audit-grid,
|
||||
.vp-realtime-command-board,
|
||||
.vp-realtime-command-grid,
|
||||
.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.getByRole('button', { name: '复制复盘包' })).toBeInTheDocument();
|
||||
expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument();
|
||||
expect(screen.getByText('查看轨迹')).toBeInTheDocument();
|
||||
@@ -928,6 +934,9 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制复盘包' }));
|
||||
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();
|
||||
|
||||
await renderDashboard();
|
||||
|
||||
Reference in New Issue
Block a user