feat(platform): add single vehicle time window delivery
This commit is contained in:
@@ -604,7 +604,7 @@ export default function App() {
|
|||||||
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
||||||
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
||||||
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
||||||
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onQueryChange={updateVehicleDetailQuery} />,
|
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onOpenMileageEvidence={openMileageWithFilters} onQueryChange={updateVehicleDetailQuery} />,
|
||||||
history: <History mode="trajectory" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
history: <History mode="trajectory" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
||||||
'history-query': <History mode="query" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
'history-query': <History mode="query" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
||||||
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} initialFilters={mileageFilters} onFiltersChange={updateMileageFilters} onOpenVehicle={openVehicle} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} />,
|
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} initialFilters={mileageFilters} onFiltersChange={updateMileageFilters} onOpenVehicle={openVehicle} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} />,
|
||||||
|
|||||||
@@ -159,6 +159,11 @@ function nextDate(value: string) {
|
|||||||
return date.toISOString().slice(0, 10);
|
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) {
|
function qualityIssueVehicleLabel(row: QualityIssueRow, fallbackVIN: string) {
|
||||||
const vin = row.vin?.trim() || fallbackVIN;
|
const vin = row.vin?.trim() || fallbackVIN;
|
||||||
return [row.plate?.trim(), vin].filter(Boolean).join(' / ') || row.phone || row.sourceEndpoint || '-';
|
return [row.plate?.trim(), vin].filter(Boolean).join(' / ') || row.phone || row.sourceEndpoint || '-';
|
||||||
@@ -185,6 +190,7 @@ export function VehicleDetail({
|
|||||||
onOpenQuality,
|
onOpenQuality,
|
||||||
onOpenHistoryEvidence,
|
onOpenHistoryEvidence,
|
||||||
onOpenRawEvidence,
|
onOpenRawEvidence,
|
||||||
|
onOpenMileageEvidence,
|
||||||
onQueryChange
|
onQueryChange
|
||||||
}: {
|
}: {
|
||||||
vin: string;
|
vin: string;
|
||||||
@@ -198,10 +204,12 @@ export function VehicleDetail({
|
|||||||
onOpenQuality?: (filters?: Record<string, string>) => void;
|
onOpenQuality?: (filters?: Record<string, string>) => void;
|
||||||
onOpenHistoryEvidence?: (filters?: Record<string, string>) => void;
|
onOpenHistoryEvidence?: (filters?: Record<string, string>) => void;
|
||||||
onOpenRawEvidence?: (filters?: Record<string, string>) => void;
|
onOpenRawEvidence?: (filters?: Record<string, string>) => void;
|
||||||
|
onOpenMileageEvidence?: (filters?: Record<string, string>) => void;
|
||||||
onQueryChange?: (keyword: string, protocol?: string) => void;
|
onQueryChange?: (keyword: string, protocol?: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = useState<VehicleQuery>({ keyword: vin, protocol });
|
const [query, setQuery] = useState<VehicleQuery>({ keyword: vin, protocol });
|
||||||
const [detail, setDetail] = useState<VehicleDetailData | null>(null);
|
const [detail, setDetail] = useState<VehicleDetailData | null>(null);
|
||||||
|
const [vehicleTimeWindow, setVehicleTimeWindow] = useState<Record<string, string>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const requestSeq = useRef(0);
|
const requestSeq = useRef(0);
|
||||||
|
|
||||||
@@ -402,6 +410,67 @@ export function VehicleDetail({
|
|||||||
protocol: activeProtocol,
|
protocol: activeProtocol,
|
||||||
filters
|
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 copyVehicleServiceURL = () => copyText(vehicleServiceURL(window.location.hash), '服务链接');
|
||||||
const copyVehicleDiagnosticSummary = () => {
|
const copyVehicleDiagnosticSummary = () => {
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
@@ -1308,6 +1377,64 @@ export function VehicleDetail({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</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="客户车辆档案摘要">
|
<Card bordered className="vp-vehicle-archive-summary" title="客户车辆档案摘要">
|
||||||
<div className="vp-vehicle-archive-summary-main">
|
<div className="vp-vehicle-archive-summary-main">
|
||||||
<div className="vp-vehicle-archive-summary-copy">
|
<div className="vp-vehicle-archive-summary-copy">
|
||||||
|
|||||||
@@ -11806,6 +11806,128 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window {
|
||||||
|
margin-top: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window .semi-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 0.72fr) minmax(0, 1.28fr);
|
||||||
|
min-height: 236px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-copy {
|
||||||
|
padding: 16px;
|
||||||
|
border-right: 1px solid var(--vp-border);
|
||||||
|
background: #f7fbff;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-copy .semi-typography {
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-controls label {
|
||||||
|
min-height: 66px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-controls label span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-controls input {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vp-text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-controls input:focus {
|
||||||
|
color: #1664ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-actions {
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-action {
|
||||||
|
min-height: 178px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: #fbfcff;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
align-content: start;
|
||||||
|
text-align: left;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-action:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-action:not(:disabled):hover,
|
||||||
|
.vp-single-vehicle-time-window-action:not(:disabled):focus-visible {
|
||||||
|
border-color: rgba(22, 100, 255, 0.42);
|
||||||
|
background: #f6fbff;
|
||||||
|
box-shadow: var(--vp-shadow-sm);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-action strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-action span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-action em {
|
||||||
|
color: #1664ff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-vehicle-task-grid {
|
.vp-vehicle-task-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
@@ -12608,6 +12730,11 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
width: min(100%, 132px);
|
width: min(100%, 132px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-single-vehicle-time-window-copy {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--vp-border);
|
||||||
|
}
|
||||||
|
|
||||||
.vp-evidence-grid,
|
.vp-evidence-grid,
|
||||||
.vp-action-grid,
|
.vp-action-grid,
|
||||||
.vp-fleet-monitor .semi-card-body,
|
.vp-fleet-monitor .semi-card-body,
|
||||||
@@ -12619,6 +12746,9 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-vehicle-live-view .semi-card-body,
|
.vp-vehicle-live-view .semi-card-body,
|
||||||
.vp-vehicle-live-main,
|
.vp-vehicle-live-main,
|
||||||
.vp-single-vehicle-action-grid,
|
.vp-single-vehicle-action-grid,
|
||||||
|
.vp-single-vehicle-time-window-main,
|
||||||
|
.vp-single-vehicle-time-window-controls,
|
||||||
|
.vp-single-vehicle-time-window-actions,
|
||||||
.vp-vehicle-live-facts,
|
.vp-vehicle-live-facts,
|
||||||
.vp-vehicle-archive-summary-main,
|
.vp-vehicle-archive-summary-main,
|
||||||
.vp-vehicle-archive-summary-grid,
|
.vp-vehicle-archive-summary-grid,
|
||||||
|
|||||||
@@ -13320,6 +13320,8 @@ test('copies vehicle service diagnostic summary', async () => {
|
|||||||
expect(screen.getByRole('button', { name: '客户服务交付 历史数据查询 查询历史' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: '客户服务交付 历史数据查询 查询历史' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '客户服务交付 告警通知 告警闭环' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: '客户服务交付 告警通知 告警闭环' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '客户服务交付 统计查询 统计复核' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: '客户服务交付 统计查询 统计复核' })).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('单车时间窗交付').length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText('把同一辆车和同一段时间带入轨迹回放、里程统计、历史导出和告警通知,客户复盘时不需要跨页面重复输入。')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: /复制归一摘要/ }));
|
fireEvent.click(screen.getByRole('button', { name: /复制归一摘要/ }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(writeText.mock.calls.some((call) => String(call[0]).includes('【统一车辆服务摘要】'))).toBe(true);
|
expect(writeText.mock.calls.some((call) => String(call[0]).includes('【统一车辆服务摘要】'))).toBe(true);
|
||||||
@@ -13423,6 +13425,18 @@ test('copies vehicle service diagnostic summary', async () => {
|
|||||||
expect(runbookCopied).toContain('验收:车辆在线状态、最新时间、有效坐标均可解释。');
|
expect(runbookCopied).toContain('验收:车辆在线状态、最新时间、有效坐标均可解释。');
|
||||||
expect(runbookCopied).toContain('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-SUMMARY-001&protocol=JT808');
|
expect(runbookCopied).toContain('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||||
expect(runbookCopied).toContain('统计查询:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808');
|
expect(runbookCopied).toContain('统计查询:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('单车开始日期'), { target: { value: '2026-07-01' } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('单车结束日期'), { target: { value: '2026-07-03' } });
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '单车时间窗交付 历史数据导出 2026-07-01 至 2026-07-03' }));
|
||||||
|
expect(window.location.hash.startsWith('#/history-query?')).toBe(true);
|
||||||
|
const windowParams = new URLSearchParams(window.location.hash.split('?')[1] ?? '');
|
||||||
|
expect(windowParams.get('keyword')).toBe('VIN-SUMMARY-001');
|
||||||
|
expect(windowParams.get('protocol')).toBe('JT808');
|
||||||
|
expect(windowParams.get('dateFrom')).toBe('2026-07-01');
|
||||||
|
expect(windowParams.get('dateTo')).toBe('2026-07-03');
|
||||||
|
expect(windowParams.get('tab')).toBe('raw');
|
||||||
|
expect(windowParams.get('includeFields')).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('exports vehicle detail service dossier as csv', async () => {
|
test('exports vehicle detail service dossier as csv', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user