feat(platform): add single vehicle time window delivery
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user