feat(platform): add single vehicle time window delivery

This commit is contained in:
lingniu
2026-07-06 01:38:00 +08:00
parent af95f69388
commit 442afb8aa5
4 changed files with 272 additions and 1 deletions

View File

@@ -159,6 +159,11 @@ function nextDate(value: string) {
return date.toISOString().slice(0, 10);
}
function scopedRangeText(dateFrom?: string, dateTo?: string) {
if (!dateFrom && !dateTo) return '未限定时间';
return `${dateFrom || '未指定'}${dateTo || '未指定'}`;
}
function qualityIssueVehicleLabel(row: QualityIssueRow, fallbackVIN: string) {
const vin = row.vin?.trim() || fallbackVIN;
return [row.plate?.trim(), vin].filter(Boolean).join(' / ') || row.phone || row.sourceEndpoint || '-';
@@ -185,6 +190,7 @@ export function VehicleDetail({
onOpenQuality,
onOpenHistoryEvidence,
onOpenRawEvidence,
onOpenMileageEvidence,
onQueryChange
}: {
vin: string;
@@ -198,10 +204,12 @@ export function VehicleDetail({
onOpenQuality?: (filters?: Record<string, string>) => void;
onOpenHistoryEvidence?: (filters?: Record<string, string>) => void;
onOpenRawEvidence?: (filters?: Record<string, string>) => void;
onOpenMileageEvidence?: (filters?: Record<string, string>) => void;
onQueryChange?: (keyword: string, protocol?: string) => void;
}) {
const [query, setQuery] = useState<VehicleQuery>({ keyword: vin, protocol });
const [detail, setDetail] = useState<VehicleDetailData | null>(null);
const [vehicleTimeWindow, setVehicleTimeWindow] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const requestSeq = useRef(0);
@@ -402,6 +410,67 @@ export function VehicleDetail({
protocol: activeProtocol,
filters
});
const vehicleTimeWindowScope = () => ({
keyword: resolvedVIN,
...(activeProtocol ? { protocol: activeProtocol } : {}),
...(vehicleTimeWindow.dateFrom?.trim() ? { dateFrom: vehicleTimeWindow.dateFrom.trim() } : {}),
...(vehicleTimeWindow.dateTo?.trim() ? { dateTo: vehicleTimeWindow.dateTo.trim() } : {})
});
const vehicleTimeWindowText = scopedRangeText(vehicleTimeWindow.dateFrom?.trim(), vehicleTimeWindow.dateTo?.trim());
const openVehicleWindowHistory = () => {
const filters = vehicleTimeWindowScope();
if (onOpenHistoryEvidence) {
onOpenHistoryEvidence(filters);
return;
}
onOpenHistory(filters.keyword, filters.protocol);
};
const openVehicleWindowMileage = () => {
const filters = vehicleTimeWindowScope();
if (onOpenMileageEvidence) {
onOpenMileageEvidence(filters);
return;
}
onOpenMileage(filters.keyword, filters.protocol);
};
const openVehicleWindowRaw = () => {
const filters = { ...vehicleTimeWindowScope(), tab: 'raw', includeFields: 'true' };
if (onOpenRawEvidence) {
onOpenRawEvidence(filters);
return;
}
onOpenRaw(filters.keyword, filters.protocol);
};
const openVehicleWindowAlerts = () => {
const filters = vehicleTimeWindowScope();
onOpenQuality?.(filters);
};
const copyVehicleTimeWindowPackage = () => {
if (!detail) {
Toast.warning('当前没有可复制的单车时间窗交付包');
return;
}
const filters = vehicleTimeWindowScope();
const rawFilters = { ...filters, tab: 'raw', includeFields: 'true' };
const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey;
const lines = [
'【单车时间窗交付包】',
`车辆:${vehicleName}`,
`时间范围:${vehicleTimeWindowText}`,
`数据通道:${activeProtocol || '全部数据通道'}`,
`服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`最新上报:${lastSeen}`,
`轨迹证据:${overview?.historyCount ?? detail.history.total ?? 0}`,
`历史数据:${overview?.rawCount ?? detail.raw.total ?? 0}`,
`统计查询:${overview?.mileageCount ?? detail.mileage.total ?? 0}`,
`告警事件:${qualityCount.toLocaleString()}`,
`轨迹回放:${vehicleServiceURL(buildAppHash({ page: 'history', keyword: filters.keyword, protocol: filters.protocol, filters }))}`,
`统计查询:${vehicleServiceURL(buildAppHash({ page: 'mileage', keyword: filters.keyword, protocol: filters.protocol, filters }))}`,
`历史导出:${vehicleServiceURL(buildAppHash({ page: 'history-query', keyword: rawFilters.keyword, protocol: rawFilters.protocol, filters: rawFilters }))}`,
`告警通知:${vehicleServiceURL(buildAppHash({ page: 'alert-events', keyword: filters.keyword, protocol: filters.protocol, filters }))}`
];
copyText(lines.join('\n'), '单车时间窗交付包');
};
const copyVehicleServiceURL = () => copyText(vehicleServiceURL(window.location.hash), '服务链接');
const copyVehicleDiagnosticSummary = () => {
if (!detail) {
@@ -1308,6 +1377,64 @@ export function VehicleDetail({
</div>
</Card>
<Card bordered className="vp-single-vehicle-time-window" title="单车时间窗交付">
<div className="vp-single-vehicle-time-window-main">
<div className="vp-single-vehicle-time-window-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={activeProtocol ? 'blue' : 'green'}>{activeProtocol || '全部数据通道'}</Tag>
<Tag color={hasResolvedVIN ? 'green' : 'orange'}>{hasResolvedVIN ? '车辆已定位' : '待绑定车辆'}</Tag>
</Space>
<Typography.Title heading={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
<div className="vp-single-vehicle-time-window-controls">
<label>
<span></span>
<input
placeholder="单车开始日期"
value={vehicleTimeWindow.dateFrom ?? ''}
onChange={(event) => setVehicleTimeWindow((previous) => ({ ...previous, dateFrom: event.currentTarget.value }))}
/>
</label>
<label>
<span></span>
<input
placeholder="单车结束日期"
value={vehicleTimeWindow.dateTo ?? ''}
onChange={(event) => setVehicleTimeWindow((previous) => ({ ...previous, dateTo: event.currentTarget.value }))}
/>
</label>
<Button size="small" theme="light" onClick={() => setVehicleTimeWindow({})}></Button>
<Button size="small" theme="light" icon={<IconCopy />} disabled={!detail} onClick={copyVehicleTimeWindowPackage}></Button>
</div>
</div>
<div className="vp-single-vehicle-time-window-actions">
{[
{ title: '轨迹回放', value: vehicleTimeWindowText, detail: '按同一时间窗回放位置、速度和里程断点。', action: '轨迹回放', color: 'blue' as const, onClick: openVehicleWindowHistory },
{ title: '里程统计', value: vehicleTimeWindowText, detail: '核对区间里程、日统计和总里程差值。', action: '统计查询', color: 'green' as const, onClick: openVehicleWindowMileage },
{ title: '历史数据导出', value: vehicleTimeWindowText, detail: '导出 RAW 与解析字段证据,缩小复盘范围。', action: '历史导出', color: 'blue' as const, onClick: openVehicleWindowRaw },
{ title: '告警通知', value: vehicleTimeWindowText, detail: '查看这辆车在同一范围内的风险事件。', action: '告警通知', color: qualityCount > 0 ? 'orange' as const : 'green' as const, onClick: openVehicleWindowAlerts }
].map((item) => (
<button
key={item.title}
type="button"
className="vp-single-vehicle-time-window-action"
disabled={!hasResolvedVIN || (item.title === '告警通知' && !onOpenQuality)}
aria-label={`单车时间窗交付 ${item.title} ${item.value}`}
onClick={item.onClick}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
</Card>
<Card bordered className="vp-vehicle-archive-summary" title="客户车辆档案摘要">
<div className="vp-vehicle-archive-summary-main">
<div className="vp-vehicle-archive-summary-copy">