From 17c839a35e482af333f7c88a96d01bcaf4f31142 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 23 Jul 2026 15:44:01 +0800 Subject: [PATCH] feat(mileage): add statistic-time sorting and refresh control --- src/modules/mileage/MonitoringView.tsx | 23 +++++++++++++--------- src/modules/mileage/api.ts | 4 ++-- src/modules/mileage/xlsx-export.ts | 9 +++++++-- src/server/routes/mileage/monitoring.ts | 26 ++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 7827f93..2caa0fe 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -446,7 +446,7 @@ const BatchMultiSelect = ({ export default function MonitoringView() { const [searchTerm, setSearchTerm] = useState(''); const [filterDept, setFilterDept] = useState('All'); - const [sortBy, setSortBy] = useState<'today' | 'total'>('today'); + const [sortBy, setSortBy] = useState<'today' | 'total' | 'statisticTime'>('today'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [isFilterOpen, setIsFilterOpen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); @@ -786,7 +786,7 @@ export default function MonitoringView() { | 车辆 {fullscreenStats.vehicleCount} | - {(fullscreenStats.vehicleCount > 0 ? (sortBy === 'today' ? fullscreenStats.totalToday : fullscreenStats.totalAll) / fullscreenStats.vehicleCount : 0).toFixed(0)} km + {(fullscreenStats.vehicleCount > 0 ? (sortBy === 'total' ? fullscreenStats.totalAll : fullscreenStats.totalToday) / fullscreenStats.vehicleCount : 0).toFixed(0)} km | 来源 @@ -1047,16 +1047,21 @@ export default function MonitoringView() { type="button" onClick={handleManualRefresh} disabled={manualRefreshing} - className="flex h-7 items-center gap-1 rounded-lg border border-blue-100 bg-blue-50 px-2 text-[9px] font-bold text-blue-600 disabled:cursor-wait disabled:opacity-60" - title="仅刷新当前筛选结果" + className="flex h-7 items-center gap-1 whitespace-nowrap rounded-lg border border-blue-100 bg-blue-50 px-2 text-[9px] font-bold text-blue-600 disabled:cursor-wait disabled:opacity-60" + title="局部刷新当前筛选数据" > - {manualRefreshing ? '刷新中' : '刷新'} + {manualRefreshing ? '刷新中' : '刷新数据'}
- +
@@ -1369,15 +1374,15 @@ export default function MonitoringView() {
-
{sortBy === 'today' ? (isRangeMode ? '区间' : '当日') : '累计'}总里程
+
{sortBy === 'total' ? '累计' : (isRangeMode ? '区间' : '当日')}总里程
- {pageLoading ?
: <>{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} km} + {pageLoading ?
: <>{Math.round(sortBy === 'total' ? stats.totalAll : stats.totalToday).toLocaleString()} km}
{rangeLabel}
平均单车
-
{pageLoading ?
: (stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}
+
{pageLoading ?
: (stats.vehicleCount > 0 ? (sortBy === 'total' ? stats.totalAll : stats.totalToday) / stats.vehicleCount : 0).toFixed(0)}
km/台
diff --git a/src/modules/mileage/api.ts b/src/modules/mileage/api.ts index 0ccbb81..6170e04 100644 --- a/src/modules/mileage/api.ts +++ b/src/modules/mileage/api.ts @@ -12,8 +12,8 @@ import { fetchJson } from '../../auth/api-client'; const BASE = '/api/mileage'; export async function fetchMonitoring(params?: { - sortBy?: string; - sortOrder?: string; + sortBy?: 'today' | 'total' | 'statisticTime'; + sortOrder?: 'asc' | 'desc'; limit?: number; page?: number; search?: string; diff --git a/src/modules/mileage/xlsx-export.ts b/src/modules/mileage/xlsx-export.ts index 7e026eb..c77ffd7 100644 --- a/src/modules/mileage/xlsx-export.ts +++ b/src/modules/mileage/xlsx-export.ts @@ -5,7 +5,7 @@ interface ExportContext { date?: string; startDate?: string; endDate?: string; - sortBy: 'today' | 'total'; + sortBy: 'today' | 'total' | 'statisticTime'; } const BASE_HEADERS = [ @@ -172,6 +172,11 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont const dateTag = start && end ? `${start.replace(/-/g, '')}-${end.replace(/-/g, '')}` : `${y}${m}${d}`; - const filename = `里程看板_${dateTag}_${hh}${mm}_${ctx.sortBy === 'today' ? (isRange ? '区间' : '今日') : '累计'}.xlsx`; + const sortLabel = ctx.sortBy === 'total' + ? '累计' + : ctx.sortBy === 'statisticTime' + ? '统计时间' + : isRange ? '区间' : '今日'; + const filename = `里程看板_${dateTag}_${hh}${mm}_${sortLabel}.xlsx`; XLSX.writeFile(wb, filename); } diff --git a/src/server/routes/mileage/monitoring.ts b/src/server/routes/mileage/monitoring.ts index f2c30f1..7165e7c 100644 --- a/src/server/routes/mileage/monitoring.ts +++ b/src/server/routes/mileage/monitoring.ts @@ -104,8 +104,11 @@ function normalizeRange(startQuery: string, endQuery: string): { start: string; } app.get('/', async (c) => { - const sortBy = c.req.query('sortBy') || 'today'; - const sortOrder = c.req.query('sortOrder') || 'desc'; + const requestedSortBy = c.req.query('sortBy'); + const sortBy = requestedSortBy === 'total' || requestedSortBy === 'statisticTime' + ? requestedSortBy + : 'today'; + const sortOrder = c.req.query('sortOrder') === 'asc' ? 'asc' : 'desc'; const limit = Number(c.req.query('limit')) || 50; const page = Number(c.req.query('page')) || 1; const date = c.req.query('date') || ''; @@ -189,9 +192,26 @@ app.get('/', async (c) => { }; const sorted = [...filtered].sort((a, b) => { + if (sortBy === 'statisticTime') { + const rawTimeA = a.dataTime || a.updatedAt || a.calculatedAt; + const rawTimeB = b.dataTime || b.updatedAt || b.calculatedAt; + const parsedTimeA = rawTimeA ? Date.parse(rawTimeA) : Number.NaN; + const parsedTimeB = rawTimeB ? Date.parse(rawTimeB) : Number.NaN; + const timeA = Number.isFinite(parsedTimeA) ? parsedTimeA : null; + const timeB = Number.isFinite(parsedTimeB) ? parsedTimeB : null; + + // Missing/invalid statistic times always stay at the end for both directions. + if (timeA == null && timeB == null) return a.plate.localeCompare(b.plate, 'zh-CN'); + if (timeA == null) return 1; + if (timeB == null) return -1; + const timeDiff = sortOrder === 'desc' ? timeB - timeA : timeA - timeB; + return timeDiff || a.plate.localeCompare(b.plate, 'zh-CN'); + } + const valA = sortBy === 'today' ? a.dailyKm : (a.totalKm || 0); const valB = sortBy === 'today' ? b.dailyKm : (b.totalKm || 0); - return sortOrder === 'desc' ? valB - valA : valA - valB; + const mileageDiff = sortOrder === 'desc' ? valB - valA : valA - valB; + return mileageDiff || a.plate.localeCompare(b.plate, 'zh-CN'); }); const offset = (page - 1) * limit;