From 0b8bbbb0638a6c89696ce3afb662f0f107bf9f0a Mon Sep 17 00:00:00 2001 From: kkfluous Date: Wed, 1 Apr 2026 23:41:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E6=97=A5=E6=9C=9F=E9=87=8C=E7=A8=8B+?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=90=9C=E7=B4=A2=E5=85=B3=E9=94=AE=E8=AF=8D?= =?UTF-8?q?=E5=92=8C=E8=BD=A6=E7=89=8C=E5=8F=B7=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端支持 date 参数,指定日期时实时查询数据库(不用缓存) - 同时查询前一天数据计算环比 - 高级筛选添加"查询日期"日期选择器 - 删除高级筛选中的"搜索关键词"和"车牌号"(已有快捷筛选) - 筛选标签支持显示日期条件 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/modules/mileage/MonitoringView.tsx | 45 +++++--------- src/modules/mileage/api.ts | 2 + src/server/routes/mileage.ts | 83 +++++++++++++++++++++++--- 3 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 74a89e7..6117266 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -104,6 +104,7 @@ export default function MonitoringView() { const [filterRegionCode, setFilterRegionCode] = useState('All'); const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' }); + const [filterDate, setFilterDate] = useState(''); const [vehicles, setVehicles] = useState([]); const [stats, setStats] = useState({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); @@ -135,6 +136,7 @@ export default function MonitoringView() { plate: filterPlate !== 'All' ? filterPlate : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, + date: filterDate || undefined, }).then(d => { setVehicles(d.vehicles); setStats(d.stats); @@ -143,7 +145,7 @@ export default function MonitoringView() { setPage(1); setHasMore(d.page < d.totalPages); }).catch(() => {}); - }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange]); + }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate]); // 加载更多 const loadMore = useCallback(() => { @@ -163,12 +165,13 @@ export default function MonitoringView() { plate: filterPlate !== 'All' ? filterPlate : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, + date: filterDate || undefined, }).then(d => { setVehicles(prev => [...prev, ...d.vehicles]); setPage(nextPage); setHasMore(nextPage < d.totalPages); }).catch(() => {}).finally(() => setLoadingMore(false)); - }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, page, loadingMore, hasMore]); + }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate, page, loadingMore, hasMore]); // 筛选/排序变化时重新加载 useEffect(() => { @@ -529,35 +532,15 @@ export default function MonitoringView() { className="overflow-hidden" >
- {/* Search Key */} + {/* Date */}
- -
- - setSearchTerm(e.target.value)} - /> -
-
- -
- {/* Plate Number */} -
- - -
- + + setFilterDate(e.target.value)} + />
{/* Project */} @@ -684,11 +667,13 @@ export default function MonitoringView() { if (appliedMileageRange.min) tags.push({ label: `里程≥${appliedMileageRange.min}`, onClear: () => { setFilterMileageRange(prev => ({ ...prev, min: '' })); setAppliedMileageRange(prev => ({ ...prev, min: '' })); } }); if (appliedMileageRange.max) tags.push({ label: `里程≤${appliedMileageRange.max}`, onClear: () => { setFilterMileageRange(prev => ({ ...prev, max: '' })); setAppliedMileageRange(prev => ({ ...prev, max: '' })); } }); if (filterRegionCode !== 'All') tags.push({ label: `地区: ${filterRegionCode}`, onClear: () => setFilterRegionCode('All') }); + if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') }); if (tags.length === 0) return null; const clearAll = () => { setFilterDept('All'); setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All'); setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All'); setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' }); + setFilterDate(''); }; return (
diff --git a/src/modules/mileage/api.ts b/src/modules/mileage/api.ts index dfd159f..d189201 100644 --- a/src/modules/mileage/api.ts +++ b/src/modules/mileage/api.ts @@ -21,6 +21,7 @@ export async function fetchMonitoring(params?: { plate?: string; mileageMin?: string; mileageMax?: string; + date?: string; }): Promise { const query = new URLSearchParams(); if (params?.sortBy) query.set('sortBy', params.sortBy); @@ -35,6 +36,7 @@ export async function fetchMonitoring(params?: { if (params?.plate) query.set('plate', params.plate); if (params?.mileageMin) query.set('mileageMin', params.mileageMin); if (params?.mileageMax) query.set('mileageMax', params.mileageMax); + if (params?.date) query.set('date', params.date); const qs = query.toString(); return fetchJson(`${BASE}/monitoring${qs ? `?${qs}` : ''}`); } diff --git a/src/server/routes/mileage.ts b/src/server/routes/mileage.ts index b56fee8..09b3497 100644 --- a/src/server/routes/mileage.ts +++ b/src/server/routes/mileage.ts @@ -152,11 +152,43 @@ async function refreshMonitoringCache() { refreshMonitoringCache(); setInterval(refreshMonitoringCache, 2 * 60 * 1000); -// GET /monitoring — 从缓存取数据,支持筛选/排序/分页 -app.get('/monitoring', (c) => { - if (!monitoringCache) { - return c.json({ vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() }); +// 查询指定日期的里程数据(非缓存) +async function queryDateMileage(dateStr: string): Promise<{ vehicles: CachedVehicle[]; yesterdayTotal: number }> { + const [mileageRows] = await mileagePool.execute( + `SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?`, + [dateStr] + ) as any; + const [yesterdayRows] = await mileagePool.execute( + `SELECT SUM(daily_km) as total FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)`, + [dateStr] + ) as any; + const [infoRows] = await pool.execute(VEHICLE_INFO_SQL) as any; + const infoMap = new Map(); + for (const row of infoRows) infoMap.set(row.plate, row); + const mileageMap = new Map(); + for (const row of mileageRows) { + const existing = mileageMap.get(row.plate); + if (!existing || Number(row.daily_km) > Number(existing.daily_km)) mileageMap.set(row.plate, row); } + const vehicles: CachedVehicle[] = Array.from(mileageMap.values()).map((m: any) => { + const info = infoMap.get(m.plate); + const dailyKm = Number(m.daily_km) || 0; + const source = m.source || 'NONE'; + return { + plate: m.plate, vin: m.vin, dailyKm, + totalKm: m.total_km !== null ? Number(m.total_km) : null, + source, isOnline: source !== 'NONE' && dailyKm > 0, isDataSynced: source !== 'NONE', + customer: info?.customer || null, department: info?.department || null, + manager: info?.manager || null, rentStatus: info?.rent_status || null, + entity: info?.entity || null, project: info?.project || null, + }; + }); + return { vehicles, yesterdayTotal: Number(yesterdayRows[0]?.total) || 0 }; +} + +// GET /monitoring — 从缓存取数据(或指定日期实时查询),支持筛选/排序/分页 +app.get('/monitoring', async (c) => { + const emptyResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() }; const sortBy = c.req.query('sortBy') || 'today'; const sortOrder = c.req.query('sortOrder') || 'desc'; @@ -170,8 +202,43 @@ app.get('/monitoring', (c) => { const mileageMin = c.req.query('mileageMin') || ''; const mileageMax = c.req.query('mileageMax') || ''; const plate = c.req.query('plate') || ''; + const date = c.req.query('date') || ''; - let vehicles = monitoringCache.vehicles; + let allVehicles: CachedVehicle[]; + let yesterdayTotal: number; + let filters: MonitoringCache['filters']; + + if (date) { + // 指定日期:实时查询 + try { + const result = await queryDateMileage(date); + allVehicles = result.vehicles; + yesterdayTotal = result.yesterdayTotal; + // 从查询结果提取筛选选项 + const deptOrder = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; + filters = { + departments: (Array.from(new Set(allVehicles.map(v => v.department).filter(Boolean))) as string[]).sort((a, b) => { + const ai = deptOrder.findIndex(d => a.includes(d)); const bi = deptOrder.findIndex(d => b.includes(d)); + return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi); + }), + customers: Array.from(new Set(allVehicles.map(v => v.customer).filter(Boolean))) as string[], + plates: allVehicles.map(v => v.plate), + projects: Array.from(new Set(allVehicles.map(v => v.project).filter(Boolean))) as string[], + entities: Array.from(new Set(allVehicles.map(v => v.entity).filter(Boolean))) as string[], + }; + } catch (e) { + console.error('monitoring date query error:', e); + return c.json(emptyResponse, 500); + } + } else { + // 默认:从缓存取 + if (!monitoringCache) return c.json(emptyResponse); + allVehicles = monitoringCache.vehicles; + yesterdayTotal = monitoringCache.stats.yesterdayTotal; + filters = monitoringCache.filters; + } + + let vehicles = allVehicles; // 筛选 if (search) { @@ -197,7 +264,7 @@ app.get('/monitoring', (c) => { totalToday: vehicles.reduce((sum, v) => sum + v.dailyKm, 0), totalAll: vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0), vehicleCount: vehicles.length, - yesterdayTotal: monitoringCache.stats.yesterdayTotal, + yesterdayTotal, }; // 排序 @@ -214,11 +281,11 @@ app.get('/monitoring', (c) => { return c.json({ vehicles: paged, stats: filteredStats, - filters: monitoringCache.filters, + filters, total, page, totalPages: Math.ceil(total / limit), - updatedAt: monitoringCache.updatedAt, + updatedAt: date || monitoringCache?.updatedAt || new Date().toISOString(), }); });