diff --git a/package-lock.json b/package-lock.json
index b443096..1652476 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ln-bi",
- "version": "1.1.22",
+ "version": "1.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ln-bi",
- "version": "1.1.22",
+ "version": "1.1.23",
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@hono/node-server": "^1.13.0",
diff --git a/package.json b/package.json
index 5cfd399..269359b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "ln-bi",
"private": true,
- "version": "1.1.22",
+ "version": "1.1.23",
"type": "module",
"scripts": {
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",
diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx
index 9ec2574..2c35af4 100644
--- a/src/modules/mileage/MonitoringView.tsx
+++ b/src/modules/mileage/MonitoringView.tsx
@@ -391,7 +391,6 @@ export default function MonitoringView() {
rangeEnd={rangeEnd}
onRangeStartChange={setRangeStart}
onRangeEndChange={setRangeEnd}
- onApplyRangePreset={applyRangePreset}
departments={departments}
filterDept={filterDept}
onFilterDeptChange={setFilterDept}
diff --git a/src/modules/mileage/monitoring/components/FilterPanel.tsx b/src/modules/mileage/monitoring/components/FilterPanel.tsx
index faf840e..b2acc98 100644
--- a/src/modules/mileage/monitoring/components/FilterPanel.tsx
+++ b/src/modules/mileage/monitoring/components/FilterPanel.tsx
@@ -1,7 +1,8 @@
import type { Dispatch, SetStateAction } from 'react';
import { motion } from 'motion/react';
import type { MonitoringFilters } from '../../types';
-import { defaultMileageDate, type RangePreset } from '../date-range';
+import { DateRangePicker } from '../../../../components/ui/DateRangePicker';
+import { defaultMileageDate, getRangePreset, type RangePreset } from '../date-range';
import BatchMultiSelect from './BatchMultiSelect';
import SearchableSelect from './SearchableSelect';
@@ -12,7 +13,6 @@ export default function FilterPanel({
rangeEnd,
onRangeStartChange,
onRangeEndChange,
- onApplyRangePreset,
departments,
filterDept,
onFilterDeptChange,
@@ -42,7 +42,6 @@ export default function FilterPanel({
rangeEnd: string;
onRangeStartChange: (value: string) => void;
onRangeEndChange: (value: string) => void;
- onApplyRangePreset: (preset: RangePreset) => void;
departments: string[];
filterDept: string;
onFilterDeptChange: (value: string) => void;
@@ -76,47 +75,25 @@ export default function FilterPanel({
className="overflow-hidden"
>
- {/* Date range */}
-
-
-
- onRangeStartChange(e.target.value)}
- />
- 至
- onRangeEndChange(e.target.value)}
- />
-
- {rangeStart && rangeEnd && rangeStart > rangeEnd &&
开始日期不能晚于结束日期
}
-
- {([
- ['today', '今天'],
- ['yesterday', '昨天'],
- ['thisWeek', '本周'],
- ['thisMonth', '本月'],
- ['last7', '近7天'],
- ['last15', '近15天'],
- ] as Array<[RangePreset, string]>).map(([preset, label]) => (
-
- ))}
-
-
+
{
+ onRangeStartChange(start);
+ onRangeEndChange(end);
+ }}
+ quickPicks={([
+ ['today', '今天'],
+ ['yesterday', '昨天'],
+ ['thisWeek', '本周'],
+ ['thisMonth', '本月'],
+ ['last7', '近7天'],
+ ['last15', '近15天'],
+ ] satisfies Array<[RangePreset, string]>).map(([preset, label]) => ({
+ label,
+ range: getRangePreset(preset),
+ }))}
+ />
{/* Department */}
diff --git a/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx b/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx
index 99b0454..bdc749c 100644
--- a/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx
+++ b/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx
@@ -331,7 +331,8 @@ export default function FullscreenMonitoring({
- {v.totalKm != null ? <>{v.totalKm.toLocaleString()} km> : 未对接}
+ {v.totalKm != null ? <>{v.totalKm.toLocaleString()} km> : 暂无读数}
+ {v.totalKmFallbackDate && 截至 {v.totalKmFallbackDate}}
|
diff --git a/src/modules/mileage/monitoring/components/VehicleList.tsx b/src/modules/mileage/monitoring/components/VehicleList.tsx
index b983eef..b88b83a 100644
--- a/src/modules/mileage/monitoring/components/VehicleList.tsx
+++ b/src/modules/mileage/monitoring/components/VehicleList.tsx
@@ -113,7 +113,8 @@ export default function VehicleList({
{v.totalKm != null ? v.totalKm.toLocaleString() : '-'}
-
km
+ {v.totalKm != null &&
km}
+ {v.totalKmFallbackDate &&
截至 {v.totalKmFallbackDate}
}
@@ -129,7 +130,8 @@ export default function VehicleList({
总
- {v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '未对接'}
+ {v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '暂无读数'}
+ {v.totalKmFallbackDate && 截至 {v.totalKmFallbackDate}}
diff --git a/src/modules/mileage/types.ts b/src/modules/mileage/types.ts
index 135af6c..4ee6733 100644
--- a/src/modules/mileage/types.ts
+++ b/src/modules/mileage/types.ts
@@ -16,6 +16,7 @@ export interface MonitoringVehicle {
mileageAnomaly?: string | null;
dailyMileage?: Record
;
totalKm: number | null;
+ totalKmFallbackDate?: string;
source: string;
sourceProtocol: MileageSourceProtocol | null;
sourceCategory: MileageSourceCategory | null;
diff --git a/src/modules/mileage/xlsx-export.ts b/src/modules/mileage/xlsx-export.ts
index 1d1ebda..dd37714 100644
--- a/src/modules/mileage/xlsx-export.ts
+++ b/src/modules/mileage/xlsx-export.ts
@@ -50,6 +50,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
...(dayKeys.length > 0 ? dailyHeaders : []),
isRange ? '区间合计(km)' : '当日里程(km)',
'累计里程(km)',
+ ...(isRange ? ['累计读数日期'] : []),
];
const summaryData: (string | number)[][] = [
@@ -71,6 +72,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
...dailyCols,
mileageCell(v, 'today'),
mileageCell(v, 'total'),
+ ...(isRange ? [v.totalKm != null ? v.totalKmFallbackDate || end : ''] : []),
];
}),
];
@@ -92,6 +94,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
for (let i = 0; i < dayKeys.length; i++) wsCols.push({ wch: 12 });
wsCols.push({ wch: 14 }); // 区间合计
wsCols.push({ wch: 14 }); // 累计里程
+ if (isRange) wsCols.push({ wch: 16 });
ws['!cols'] = wsCols;
// 冻结首行 + 固定列
@@ -126,6 +129,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
...dayKeys.map(day => `${day}里程(km)`),
'区间合计(km)',
'累计里程(km)',
+ ...(isRange ? ['累计读数日期'] : []),
];
const detailData: (string | number)[][] = [
detailHeaders,
@@ -140,6 +144,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
...dayKeys.map(day => v.dailyMileage?.[day] ?? '—'),
mileageCell(v, 'today'),
v.totalKm != null ? v.totalKm : '',
+ ...(isRange ? [v.totalKm != null ? v.totalKmFallbackDate || end : ''] : []),
]),
];
const detailWs = XLSX.utils.aoa_to_sheet(detailData);
@@ -154,6 +159,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
...dayKeys.map(() => ({ wch: 14 })),
{ wch: 14 },
{ wch: 14 },
+ ...(isRange ? [{ wch: 16 }] : []),
];
detailWs['!freeze'] = { xSplit: 7, ySplit: 1 } as never;
for (let r = 1; r < detailData.length; r++) {
diff --git a/src/server/routes/mileage/cache.ts b/src/server/routes/mileage/cache.ts
index b8a9d34..40137c1 100644
--- a/src/server/routes/mileage/cache.ts
+++ b/src/server/routes/mileage/cache.ts
@@ -127,7 +127,7 @@ export async function queryRangeMileage(
]);
const { summary, yesterdayRows } = cached.result;
return {
- vehicles: buildSummaryMileage(summary, yesterdayRows, infoMap, targetRows, regionMap),
+ vehicles: buildSummaryMileage(summary, yesterdayRows, infoMap, targetRows, regionMap, cached.result.priorOdometers),
summary,
start: startDate, end: endDate,
queriedAt: cached.queriedAt,
diff --git a/src/server/routes/mileage/monitoring-cache.test.ts b/src/server/routes/mileage/monitoring-cache.test.ts
index cb9f363..5628a71 100644
--- a/src/server/routes/mileage/monitoring-cache.test.ts
+++ b/src/server/routes/mileage/monitoring-cache.test.ts
@@ -54,11 +54,14 @@ test('summary integration preserves scope, valid partial mileage, trend nulls, r
assert.equal(first.vehicles[0].plate,'TEST1');assert.equal(first.stats.totalToday,50);
assert.equal(first.rangeDailyTotals?.[1].totalKm,null);
assert.equal(requests.filter(r=>r.url.endsWith('/summary/query')).length,1);
- assert.equal(requests.filter(r=>r.url.endsWith('/range/query')).length,0);
+ assert.deepEqual(requests.filter(r=>r.url.endsWith('/range/query')).map(r=>r.body.plateNumbers), [['TEST0']]);
const second=await read('&page=2&limit=1&includeDailyTrend=0');assert.equal(second.vehicles[0].periodMileageKm,20);
const sorted=await read('&sortOrder=asc&includeDailyTrend=0');assert.equal(sorted.vehicles[0].periodMileageKm,0);
const exported=await read('&limit=9999&includeDailyTrend=0');
assert.equal(exported.vehicles.length,4);assert.equal(exported.vehicles[3].periodMileageKm,null);
+ const fallback = exported.vehicles.find(v => v.plate === 'TEST0')!;
+ assert.equal(fallback.totalKm,100);
+ assert.equal(fallback.totalKmFallbackDate,'2026-10-10');
assert.equal(exported.queriedAt,first.queriedAt);assert.ok(!exported.vehicles[0].dailyMileage);
assert.equal(requests.filter(r=>r.url.endsWith('/summary/query')).length,1);
user={...user,permissionLevel:'department'};
diff --git a/src/server/routes/mileage/query-cache.ts b/src/server/routes/mileage/query-cache.ts
index 2ad6413..f963ae8 100644
--- a/src/server/routes/mileage/query-cache.ts
+++ b/src/server/routes/mileage/query-cache.ts
@@ -1,5 +1,5 @@
-import { fetchOneOsDailyMileage, type OneOsDailyMileage } from './oneos-api.js';
-import { previousMileageDate } from './cache-model.js';
+import { fetchOneOsDailyMileage, fetchOneOsMileageDates, type OneOsDailyMileage } from './oneos-api.js';
+import { mileageDatesBetween, previousMileageDate } from './cache-model.js';
import { fetchMileageSummary, type MileageSummary } from './summary-api.js';
import type { OneOsProtocol } from './source-policy.js';
@@ -10,6 +10,7 @@ const MAX_ENTRIES = 4;
export interface RangeMileageSource {
summary: MileageSummary;
yesterdayRows: OneOsDailyMileage[];
+ priorOdometers?: Map;
}
type Entry = { result: RangeMileageSource; queriedAt: string; expiresAt: number };
@@ -19,7 +20,23 @@ async function loadSource(start: string, end: string, protocols: OneOsProtocol[]
fetchMileageSummary(start, end, protocols, plates),
plates ? Promise.resolve([]) : fetchOneOsDailyMileage(previousMileageDate(start), undefined, protocols, refresh),
]);
- return { summary, yesterdayRows };
+ // Plate-scoped summary requests are used only for filtered trends.
+ if (plates || start === end) return { summary, yesterdayRows };
+ const missing = summary.vehicles.filter(v => v.endDateTotalMileageKm === null && v.validDays + v.anomalyDays > 0);
+ if (!missing.length) return { summary, yesterdayRows };
+ const dates = mileageDatesBetween(start, previousMileageDate(end));
+ const history = await fetchOneOsMileageDates(dates, missing.map(v => v.plateNumber), protocols);
+ const priorOdometers = new Map();
+ for (const date of dates.reverse()) {
+ for (const row of history.get(date) || []) {
+ const plate = row.plateNumber.toUpperCase();
+ const trusted = row.status === 'NORMAL' || row.dataQuality === 'BASELINE_MISSING' || row.dataQuality === 'ODOMETER_SOURCE_CHANGED';
+ if (!priorOdometers.has(plate) && trusted && row.dataQuality !== 'CARRIED_FORWARD' && row.totalMileageKm != null) {
+ priorOdometers.set(plate, { date, km: row.totalMileageKm });
+ }
+ }
+ }
+ return { summary, yesterdayRows, priorOdometers };
}
export function createMonitoringQueryCache(
diff --git a/src/server/routes/mileage/summary-model.ts b/src/server/routes/mileage/summary-model.ts
index e26529b..b7312a8 100644
--- a/src/server/routes/mileage/summary-model.ts
+++ b/src/server/routes/mileage/summary-model.ts
@@ -4,15 +4,17 @@ import { buildPlateTargetNamesMap, dailyMileageMap, mergeMonitoringVehicles, typ
import type { CachedVehicle, VehicleInfoRow } from './types.js';
import { sourceCategoryFromProtocol } from './source-policy.js';
-export function buildSummaryMileage(summary: MileageSummary, yesterdayRows: OneOsDailyMileage[], infoMap: Map, targets: TargetRow[], regions: Record): CachedVehicle[] {
+export function buildSummaryMileage(summary: MileageSummary, yesterdayRows: OneOsDailyMileage[], infoMap: Map, targets: TargetRow[], regions: Record, priorOdometers?: Map): CachedVehicle[] {
const byPlate = new Map(summary.vehicles.map(v => [v.plateNumber, v]));
return mergeMonitoringVehicles([], infoMap, dailyMileageMap(yesterdayRows), buildPlateTargetNamesMap(targets), regions).map(vehicle => {
const row = byPlate.get(vehicle.plate.trim().toUpperCase());
if (!row) return { ...vehicle, periodMileageKm: null };
+ const prior = priorOdometers?.get(row.plateNumber);
const categories = new Set(row.sourceProtocols.map(sourceCategoryFromProtocol));
return { ...vehicle, vin: row.vin || vehicle.vin,
dailyKm: row.periodMileageKm ?? 0, periodMileageKm: row.periodMileageKm,
- totalKm: row.endDateTotalMileageKm,
+ totalKm: row.endDateTotalMileageKm ?? prior?.km ?? null,
+ totalKmFallbackDate: prior?.date,
validDays: row.validDays, missingDays: row.missingDays, anomalyDays: row.anomalyDays,
qualityStatus: row.status, qualityReasons: row.qualityReasons, sourceProtocols: row.sourceProtocols,
mileageAnomaly: row.anomalyDays ? row.qualityReasons.join(', ') || 'DATA_ANOMALY' : null,
diff --git a/src/server/routes/mileage/types.ts b/src/server/routes/mileage/types.ts
index d4421b6..1223304 100644
--- a/src/server/routes/mileage/types.ts
+++ b/src/server/routes/mileage/types.ts
@@ -13,6 +13,7 @@ export interface CachedVehicle {
mileageAnomaly?: string | null;
dailyMileage?: Record;
totalKm: number | null;
+ totalKmFallbackDate?: string;
source: string;
sourceProtocol: 'GB32960' | 'MQTT' | 'JT808' | null;
sourceCategory: 'INSTRUMENT' | 'GPS' | 'MIXED' | null;