diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 43cc730..74c97ff 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -106,7 +106,8 @@ export default function MonitoringView() { const [filterProject, setFilterProject] = useState('All'); const [filterEntity, setFilterEntity] = useState('All'); const [filterRentStatus, setFilterRentStatus] = useState('All'); - const [filterRegionCode, setFilterRegionCode] = useState('All'); + const [filterPlatePrefix, setFilterPlatePrefix] = useState('All'); + const [filterTargetName, setFilterTargetName] = useState('All'); const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' }); const [filterDate, setFilterDate] = useState(() => { @@ -117,7 +118,7 @@ export default function MonitoringView() { const [vehicles, setVehicles] = useState([]); const [stats, setStats] = useState({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); - const [filterOptions, setFilterOptions] = useState({ departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [] }); + const [filterOptions, setFilterOptions] = useState({ departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [] }); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); @@ -141,6 +142,8 @@ export default function MonitoringView() { project: filterProject !== 'All' ? filterProject : undefined, entity: filterEntity !== 'All' ? filterEntity : undefined, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, + platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, + targetName: filterTargetName !== 'All' ? filterTargetName : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, @@ -153,7 +156,7 @@ export default function MonitoringView() { setPage(1); setHasMore(d.page < d.totalPages); }).catch(() => {}); - }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlate, appliedMileageRange, filterDate]); + }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetName, filterPlate, appliedMileageRange, filterDate]); // 加载更多 const loadMore = useCallback(() => { @@ -171,6 +174,8 @@ export default function MonitoringView() { project: filterProject !== 'All' ? filterProject : undefined, entity: filterEntity !== 'All' ? filterEntity : undefined, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, + platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, + targetName: filterTargetName !== 'All' ? filterTargetName : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, @@ -226,8 +231,9 @@ export default function MonitoringView() { }, []); const scrollToTop = () => { - window.scrollTo({ top: 0, behavior: 'smooth' }); + window.scrollTo(0, 0); document.documentElement.scrollTop = 0; + document.body.scrollTop = 0; }; const filteredVehicles = vehicles; @@ -249,6 +255,8 @@ export default function MonitoringView() { dept: filterDept !== 'All' ? filterDept : undefined, customer: filterCustomer !== 'All' ? filterCustomer : undefined, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, + platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, + targetName: filterTargetName !== 'All' ? filterTargetName : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined, date: filterDate || undefined, }).then(d => { @@ -626,18 +634,29 @@ export default function MonitoringView() { - {/* Region Code */} + {/* Target Name */}
- + +
+ + {/* Plate Prefix */} +
+ +
@@ -674,7 +693,7 @@ export default function MonitoringView() { setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All'); - setFilterRegionCode('All'); + setFilterPlatePrefix('All'); setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' }); }} @@ -709,12 +728,13 @@ export default function MonitoringView() { if (searchTerm) tags.push({ label: `搜索: ${searchTerm}`, onClear: () => setSearchTerm('') }); 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 (filterTargetName !== 'All') tags.push({ label: `批次: ${filterTargetName}`, onClear: () => setFilterTargetName('All') }); + if (filterPlatePrefix !== 'All') tags.push({ label: `区域: ${filterPlatePrefix}`, onClear: () => setFilterPlatePrefix('All') }); if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') }); if (tags.length === 0) return null; const clearAll = () => { setFilterDept('All'); setFilterCustomer('All'); setFilterRentStatus('All'); setFilterProject('All'); setFilterEntity('All'); - setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All'); + setFilterPlate('All'); setSearchTerm(''); setFilterPlatePrefix('All'); setFilterTargetName('All'); setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' }); setFilterDate(''); }; @@ -741,11 +761,6 @@ export default function MonitoringView() {
{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} km - {sortBy === 'today' && stats.yesterdayTotal > 0 && (() => { - const change = ((stats.totalToday - stats.yesterdayTotal) / stats.yesterdayTotal) * 100; - const isUp = change >= 0; - return {isUp ? '\u2191' : '\u2193'}{Math.abs(change).toFixed(1)}%; - })()}
diff --git a/src/modules/mileage/api.ts b/src/modules/mileage/api.ts index 58d8a23..41c0bf0 100644 --- a/src/modules/mileage/api.ts +++ b/src/modules/mileage/api.ts @@ -19,6 +19,8 @@ export async function fetchMonitoring(params?: { project?: string; entity?: string; rentStatus?: string; + platePrefix?: string; + targetName?: string; plate?: string; mileageMin?: string; mileageMax?: string; @@ -35,6 +37,8 @@ export async function fetchMonitoring(params?: { if (params?.project) query.set('project', params.project); if (params?.entity) query.set('entity', params.entity); if (params?.rentStatus) query.set('rentStatus', params.rentStatus); + if (params?.platePrefix) query.set('platePrefix', params.platePrefix); + if (params?.targetName) query.set('targetName', params.targetName); if (params?.plate) query.set('plate', params.plate); if (params?.mileageMin) query.set('mileageMin', params.mileageMin); if (params?.mileageMax) query.set('mileageMax', params.mileageMax); diff --git a/src/modules/mileage/types.ts b/src/modules/mileage/types.ts index b5b5a02..9379ed4 100644 --- a/src/modules/mileage/types.ts +++ b/src/modules/mileage/types.ts @@ -28,6 +28,8 @@ export interface MonitoringFilters { projects: string[]; entities: string[]; rentStatuses: string[]; + platePrefixes: { prefix: string; count: number }[]; + targetNames: string[]; } export interface MonitoringData { diff --git a/src/server/routes/mileage.ts b/src/server/routes/mileage.ts index dc9a4e2..663f9e3 100644 --- a/src/server/routes/mileage.ts +++ b/src/server/routes/mileage.ts @@ -45,7 +45,8 @@ interface CachedVehicle { interface MonitoringCache { vehicles: CachedVehicle[]; stats: { totalToday: number; totalAll: number; vehicleCount: number }; - filters: { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[]; rentStatuses: string[] }; + filters: { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[]; rentStatuses: string[]; platePrefixes: { prefix: string; count: number }[]; targetNames: string[] }; + targetPlatesMap: Map>; updatedAt: string; } @@ -57,7 +58,7 @@ async function refreshMonitoringCache() { const start = Date.now(); // 并行查询两个数据库 - const [mileageResult, yesterdayResult, infoRows] = await Promise.all([ + const [mileageResult, yesterdayResult, infoRows, targetRows] = await Promise.all([ (async () => { const [dateRows] = await mileagePool.execute( 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' @@ -85,6 +86,9 @@ async function refreshMonitoringCache() { return map; })(), pool.execute(VEHICLE_INFO_SQL).then(([rows]) => rows as any[]), + pool.execute(`SELECT t.id, t.target_name, v.plate_number FROM tab_mileage_assessment_target t + JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 + WHERE t.is_deleted = 0`).then(([rows]) => rows as any[]), ]); // 车辆关联信息 map @@ -93,6 +97,17 @@ async function refreshMonitoringCache() { infoMap.set(row.plate, row); } + // 型号批次→车牌映射 + const targetNameMap = new Map(); + const targetPlatesMap = new Map>(); // targetName -> plates + for (const r of targetRows) { + targetNameMap.set(r.id, r.target_name); + const set = targetPlatesMap.get(r.target_name) || new Set(); + set.add(r.plate_number); + targetPlatesMap.set(r.target_name, set); + } + const targetNames = Array.from(targetPlatesMap.keys()); + // 去重:同一 plate 取 daily_km 最大的 const mileageMap = new Map(); for (const row of mileageResult) { @@ -143,11 +158,18 @@ async function refreshMonitoringCache() { const projects = Array.from(new Set(vehicles.map(v => v.project).filter(Boolean))) as string[]; const entities = Array.from(new Set(vehicles.map(v => v.entity).filter(Boolean))) as string[]; const rentStatuses = Array.from(new Set(vehicles.map(v => v.rentStatus).filter(Boolean))) as string[]; + const prefixCount = new Map(); + for (const v of vehicles) { + const p = v.plate.charAt(0); + prefixCount.set(p, (prefixCount.get(p) || 0) + 1); + } + const platePrefixes = Array.from(prefixCount.entries()).map(([prefix, count]) => ({ prefix, count })).sort((a, b) => b.count - a.count); monitoringCache = { vehicles, stats: { totalToday, totalAll, vehicleCount: vehicles.length }, - filters: { departments, customers, plates, projects, entities, rentStatuses }, + filters: { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames }, + targetPlatesMap, updatedAt: new Date().toISOString(), }; @@ -200,7 +222,7 @@ async function queryDateMileage(dateStr: string): Promise<{ vehicles: CachedVehi // 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: [], rentStatuses: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() }; + const emptyResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() }; const sortBy = c.req.query('sortBy') || 'today'; const sortOrder = c.req.query('sortOrder') || 'desc'; @@ -213,6 +235,8 @@ app.get('/monitoring', async (c) => { const entity = c.req.query('entity') || ''; const mileageMin = c.req.query('mileageMin') || ''; const mileageMax = c.req.query('mileageMax') || ''; + const platePrefix = c.req.query('platePrefix') || ''; + const targetName = c.req.query('targetName') || ''; const plate = c.req.query('plate') || ''; const rentStatus = c.req.query('rentStatus') || ''; const date = c.req.query('date') || ''; @@ -236,6 +260,8 @@ app.get('/monitoring', async (c) => { 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[], rentStatuses: Array.from(new Set(allVehicles.map(v => v.rentStatus).filter(Boolean))) as string[], + platePrefixes: (() => { const m = new Map(); for (const v of allVehicles) { const p = v.plate.charAt(0); m.set(p, (m.get(p) || 0) + 1); } return Array.from(m.entries()).map(([prefix, count]) => ({ prefix, count })).sort((a, b) => b.count - a.count); })(), + targetNames: monitoringCache?.filters.targetNames || [], }; } catch (e) { console.error('monitoring date query error:', e); @@ -264,6 +290,12 @@ app.get('/monitoring', async (c) => { if (entity) vehicles = vehicles.filter(v => v.entity === entity); if (rentStatus) vehicles = vehicles.filter(v => v.rentStatus === rentStatus); if (plate) vehicles = vehicles.filter(v => v.plate === plate); + if (platePrefix) vehicles = vehicles.filter(v => v.plate.startsWith(platePrefix)); + if (targetName && monitoringCache?.targetPlatesMap) { + const tPlates = monitoringCache.targetPlatesMap.get(targetName); + if (tPlates) vehicles = vehicles.filter(v => tPlates.has(v.plate)); + else vehicles = []; + } if (mileageMin) vehicles = vehicles.filter(v => v.dailyKm >= Number(mileageMin)); if (mileageMax) vehicles = vehicles.filter(v => v.dailyKm <= Number(mileageMax));