import { Hono } from 'hono'; import { getCache, queryDateMileage, queryRangeMileage, buildDateFilters } from './cache.js'; import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js'; import type { AuthUser } from '../../auth/types.js'; import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js'; const app = new Hono(); const EMPTY_RESPONSE: MonitoringResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString(), }; function applyFilters(vehicles: CachedVehicle[], params: { search: string; dept: string; customer: string; project: string; entity: string; rentStatus: string; plate: string; platePrefix: string; targetNames: string[]; region: string; mileageMin: string; mileageMax: string; }): CachedVehicle[] { let result = vehicles; if (params.search) { const q = params.search.toLowerCase(); result = result.filter(v => v.plate.toLowerCase().includes(q) || (v.customer || '').toLowerCase().includes(q) || (v.project || '').toLowerCase().includes(q) ); } if (params.dept) result = result.filter(v => params.dept === '__EMPTY__' ? !v.department : v.department === params.dept); if (params.customer) result = result.filter(v => params.customer === '__EMPTY__' ? !v.customer : v.customer === params.customer); if (params.project) result = result.filter(v => v.project === params.project); if (params.entity) result = result.filter(v => v.entity === params.entity); if (params.rentStatus) result = result.filter(v => v.rentStatus === params.rentStatus); if (params.plate) { const wanted = new Set(params.plate.split(',').map(s => s.trim()).filter(Boolean)); if (wanted.size > 0) result = result.filter(v => wanted.has(v.plate)); } if (params.platePrefix) result = result.filter(v => v.plate.startsWith(params.platePrefix)); if (params.region) result = result.filter(v => v.region === params.region); if (params.targetNames.length > 0) { const selectedTargets = new Set(params.targetNames); result = result.filter(v => v.targetNames.some(targetName => selectedTargets.has(targetName))); } if (params.mileageMin) result = result.filter(v => v.dailyKm >= Number(params.mileageMin)); if (params.mileageMax) result = result.filter(v => v.dailyKm <= Number(params.mileageMax)); return result; } function parseTargetNames(reqUrl: string): string[] { const params = new URL(reqUrl).searchParams; const raw = [ ...params.getAll('targetName'), ...params.getAll('targetNames'), ]; const names = raw.flatMap(item => item.split(',')) .map(item => item.trim()) .filter(Boolean); return Array.from(new Set(names)); } function parseYmd(value: string): Date | null { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); if (!match) return null; const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); date.setHours(0, 0, 0, 0); return Number.isFinite(date.getTime()) ? date : null; } function fmtYmd(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function normalizeRange(startQuery: string, endQuery: string): { start: string; end: string } | null { if (!startQuery && !endQuery) return null; const start = parseYmd(startQuery || endQuery); const end = parseYmd(endQuery || startQuery); if (!start || !end) return null; const a = start <= end ? start : end; let b = start <= end ? end : start; const span = Math.round((b.getTime() - a.getTime()) / 86400000) + 1; if (span > 366) { b = new Date(a); b.setDate(a.getDate() + 365); } return { start: fmtYmd(a), end: fmtYmd(b) }; } app.get('/', async (c) => { const sortBy = c.req.query('sortBy') || 'today'; const sortOrder = c.req.query('sortOrder') || 'desc'; const limit = Number(c.req.query('limit')) || 50; const page = Number(c.req.query('page')) || 1; const date = c.req.query('date') || ''; const range = normalizeRange(c.req.query('startDate') || '', c.req.query('endDate') || ''); const filterParams = { search: c.req.query('search') || '', dept: c.req.query('dept') || '', customer: c.req.query('customer') || '', project: c.req.query('project') || '', entity: c.req.query('entity') || '', rentStatus: c.req.query('rentStatus') || '', plate: c.req.query('plate') || '', platePrefix: c.req.query('platePrefix') || '', targetNames: parseTargetNames(c.req.url), region: c.req.query('region') || '', mileageMin: c.req.query('mileageMin') || '', mileageMax: c.req.query('mileageMax') || '', }; let allVehicles: CachedVehicle[]; let filters: MonitoringFilters; let rangeDailyTotals: { date: string; totalKm: number }[] | undefined; let dateRange: { start: string; end: string } | undefined; if (range) { try { const result = await queryRangeMileage(range.start, range.end); allVehicles = result.vehicles; rangeDailyTotals = result.dailyTotals; dateRange = { start: result.start, end: result.end }; filters = buildDateFilters(allVehicles); } catch (e: unknown) { console.error('monitoring range query error:', e); return c.json(EMPTY_RESPONSE, 500); } } else if (date) { try { allVehicles = await queryDateMileage(date); filters = buildDateFilters(allVehicles); } catch (e: unknown) { console.error('monitoring date query error:', e); return c.json(EMPTY_RESPONSE, 500); } } else { const cache = getCache(); if (!cache) return c.json(EMPTY_RESPONSE); allVehicles = cache.vehicles; filters = cache.filters; } // 权限过滤 const user = (c as any).get('user') as AuthUser | undefined; if (user) { allVehicles = filterByPermission(allVehicles, user); filters = buildDateFilters(allVehicles); // 重算筛选选项以匹配权限范围 } // 区域级联:选中运营区域时,下游筛选选项(车牌等)只展示该区域车辆 if (filterParams.region) { const regionScope = allVehicles.filter(v => v.region === filterParams.region); filters = buildDateFilters(regionScope); } const filtered = applyFilters(allVehicles, filterParams); if (rangeDailyTotals && filtered.length !== allVehicles.length) { rangeDailyTotals = rangeDailyTotals.map(item => ({ ...item, totalKm: filtered.reduce((sum, vehicle) => sum + (vehicle.dailyMileage?.[item.date] || 0), 0), })); } const stats = { totalToday: filtered.reduce((sum, v) => sum + v.dailyKm, 0), totalAll: filtered.reduce((sum, v) => sum + (v.totalKm || 0), 0), vehicleCount: filtered.length, yesterdayTotal: filtered.reduce((sum, v) => sum + v.yesterdayKm, 0), }; const sorted = [...filtered].sort((a, b) => { 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 offset = (page - 1) * limit; const paged = sorted.slice(offset, offset + limit); const total = filtered.length; return c.json({ vehicles: maskCustomerNames(paged), stats, filters, rangeDailyTotals, dateRange, total, page, totalPages: Math.ceil(total / limit), updatedAt: dateRange?.end || date || getCache()?.updatedAt || new Date().toISOString(), }); }); export default app;