fix(platform): harden queries and batch vehicle search

This commit is contained in:
lingniu
2026-07-16 00:14:16 +08:00
parent c29ccdf2da
commit 53e1b57e86
19 changed files with 193 additions and 53 deletions

View File

@@ -6,6 +6,7 @@ import { api } from '../../api/client';
import type { DailyMileageRow, MileageStatistics, VehicleRow } from '../../api/types';
import { downloadMileageWorkbook } from '../domain/mileageExport';
import { InlineError } from '../shared/AsyncState';
import { QUERY_MEMORY } from '../queryPolicy';
const DAY = 86_400_000;
const DETAIL_LIMIT = 10_000;
@@ -134,9 +135,10 @@ function VehicleMultiSelect({ value, onChange }: { value: VehicleOption[]; onCha
}, [debounced]);
const candidates = useQuery({
queryKey: ['mileage-vehicle-options', candidateParams.toString()],
queryFn: () => api.vehicles(candidateParams),
queryFn: ({ signal }) => api.vehicles(candidateParams, signal),
enabled: open,
staleTime: 60_000
staleTime: 30_000,
gcTime: QUERY_MEMORY.optionGcTime
});
const selected = useMemo(() => new Set(value.map((vehicle) => vehicle.vin)), [value]);
const options = (candidates.data?.items ?? []).filter((vehicle, index, rows) => rows.findIndex((item) => item.vin === vehicle.vin) === index);
@@ -174,7 +176,7 @@ function SummaryRail({ data, criteria, fleetTotal }: { data?: MileageStatistics;
const days = inclusiveDays(criteria.dateFrom, criteria.dateTo);
const vehicleCount = criteria.vehicles.length ? data?.vehicleCount ?? 0 : fleetTotal ?? 0;
const items = [
['查询车辆', `${vehicleCount}`, criteria.vehicles.length ? `已选择 ${criteria.vehicles.length}` : `全部车辆 · ${data?.vehicleCount ?? 0} 辆有里程`],
['已绑定主车辆', `${vehicleCount}`, criteria.vehicles.length ? `已选择 ${criteria.vehicles.length}` : `档案口径 · ${data?.vehicleCount ?? 0} 辆有里程`],
['统计天数', `${days}`, `${criteria.dateFrom}${criteria.dateTo}`],
['区间总里程', `${formatKm(data?.periodMileageKm)} km`, `${data?.recordCount ?? 0} 条车辆日记录`],
['日均里程', `${formatKm(data?.averageDailyMileageKm)} km`, '按有效车辆日平均']
@@ -225,9 +227,10 @@ export default function StatisticsPage() {
const fleetParams = useMemo(() => new URLSearchParams({ limit: String(PAGE_SIZE), offset: String((page - 1) * PAGE_SIZE), bindingStatus: 'bound' }), [page]);
const fleetVehicles = useQuery({
queryKey: ['mileage-fleet-page', fleetParams.toString()],
queryFn: () => api.vehicleCoverage(fleetParams),
queryFn: ({ signal }) => api.vehicleCoverage(fleetParams, signal),
enabled: !hasVehicles,
staleTime: 60_000
staleTime: 60_000,
gcTime: QUERY_MEMORY.summaryGcTime
});
const displayVehicles = useMemo<VehicleOption[]>(() => hasVehicles
? criteria.vehicles
@@ -235,8 +238,8 @@ export default function StatisticsPage() {
const statisticsParams = useMemo(() => mileageParams(criteria, -1), [criteria]);
const rowsCriteria = useMemo(() => ({ ...criteria, vehicles: displayVehicles }), [criteria, displayVehicles]);
const rowsParams = useMemo(() => mileageParams(rowsCriteria, 0), [rowsCriteria]);
const statistics = useQuery({ queryKey: ['mileage-statistics', statisticsParams.toString()], queryFn: () => api.mileageStatistics(statisticsParams), staleTime: 60_000, placeholderData: (previous) => previous });
const mileage = useQuery({ queryKey: ['daily-mileage-query', rowsParams.toString()], queryFn: () => api.dailyMileage(rowsParams), enabled: displayVehicles.length > 0, staleTime: 60_000, placeholderData: (previous) => previous });
const statistics = useQuery({ queryKey: ['mileage-statistics', statisticsParams.toString()], queryFn: ({ signal }) => api.mileageStatistics(statisticsParams, signal), staleTime: 60_000, gcTime: QUERY_MEMORY.summaryGcTime, placeholderData: (previous) => previous });
const mileage = useQuery({ queryKey: ['daily-mileage-query', rowsParams.toString()], queryFn: ({ signal }) => api.dailyMileage(rowsParams, signal), enabled: displayVehicles.length > 0, staleTime: 60_000, gcTime: QUERY_MEMORY.highVolumeGcTime, placeholderData: (previous) => previous });
const totals = useMemo(() => new Map((statistics.data?.ranking ?? []).map((row) => [row.vin, row.mileageKm])), [statistics.data?.ranking]);
const dates = useMemo(() => rangeDates(criteria.dateFrom, criteria.dateTo), [criteria.dateFrom, criteria.dateTo]);
const matrixRows = useMemo(() => displayVehicles.map((vehicle) => {
@@ -304,7 +307,7 @@ export default function StatisticsPage() {
};
return <div className="v2-mileage-page">
<header className="v2-mileage-heading"><div><h2></h2><p></p></div><button type="button" onClick={() => { statistics.refetch(); mileage.refetch(); if (!hasVehicles) fleetVehicles.refetch(); }} disabled={refreshing}><IconRefresh />{refreshing ? '更新中' : '刷新数据'}</button></header>
<header className="v2-mileage-heading"><div><h2></h2><p></p></div><button type="button" onClick={() => { statistics.refetch(); mileage.refetch(); if (!hasVehicles) fleetVehicles.refetch(); }} disabled={refreshing}><IconRefresh />{refreshing ? '更新中' : '刷新数据'}</button></header>
<section className="v2-mileage-query-panel">
<form className="v2-mileage-filter" onSubmit={submit}>
<VehicleMultiSelect value={draft.vehicles} onChange={(vehicles) => setDraft((current) => ({ ...current, vehicles }))} />
@@ -323,6 +326,6 @@ export default function StatisticsPage() {
{!fleetVehicles.isLoading && !displayVehicles.length ? <div className="v2-mileage-empty"></div> : null}
<footer><span>{hasVehicles ? `已选择 ${totalVehicles} 辆车辆` : `${page} / ${totalPages} 页 · 共 ${totalVehicles} 辆 · 每页 ${PAGE_SIZE}`}{exportFeedback ? ` · ${exportFeedback}` : ''}</span>{!hasVehicles && totalVehicles ? <div><button type="button" disabled={page <= 1 || fleetVehicles.isFetching} onClick={() => setPage((current) => Math.max(1, current - 1))}></button><button type="button" disabled={page >= totalPages || fleetVehicles.isFetching} onClick={() => setPage((current) => Math.min(totalPages, current + 1))}></button></div> : null}</footer>
</section>
<footer className="v2-mileage-evidence"><span>{statistics.data?.asOf || '—'}</span><span>{criteria.sources.filter((source) => source.enabled).map((source) => source.protocol).join(' ')}</span><span> 1 </span></footer>
<footer className="v2-mileage-evidence"><span>{statistics.data?.asOf || '—'}</span><span>{criteria.sources.filter((source) => source.enabled).map((source) => source.protocol).join(' ')}</span><span> 1 · </span></footer>
</div>;
}