From 1fb9d53873853207afbd99d5cae0533aa0c40b56 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Wed, 1 Apr 2026 22:11:52 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=AE=9E=E6=97=B6=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:车辆关联信息缓存5分钟、两库并行查询、支持服务端 筛选/排序/分页(默认返回100条) - 前端:筛选和排序参数传给后端,不再加载全量数据 - 筛选选项(部门/客户/车牌)仅首次加载获取 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/modules/mileage/MonitoringView.tsx | 67 +++++++++--------- src/modules/mileage/api.ts | 21 +++++- src/modules/mileage/types.ts | 1 + src/server/routes/mileage.ts | 96 +++++++++++++++++++------- 4 files changed, 125 insertions(+), 60 deletions(-) diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 1b08de3..0104bfe 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -106,14 +106,43 @@ export default function MonitoringView() { const [filterRegionCode, setFilterRegionCode] = useState('All'); const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); - const [allVehicles, setAllVehicles] = useState([]); + const [filteredVehicles, setFilteredVehicles] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [departments, setDepartments] = useState([]); + const [plateNumbers, setPlateNumbers] = useState([]); + const [projects, setProjects] = useState([]); + // 加载筛选选项(仅首次,用轻量请求) useEffect(() => { - const load = () => fetchMonitoring().then(d => setAllVehicles(d.vehicles)).catch(() => {}); + fetchMonitoring({ limit: 2000 }).then(d => { + const depts = Array.from(new Set(d.vehicles.map(v => v.department).filter(Boolean))) as string[]; + const plates = Array.from(new Set(d.vehicles.map(v => v.plate).filter(Boolean))); + const custs = Array.from(new Set(d.vehicles.map(v => v.customer).filter(Boolean))) as string[]; + setDepartments(depts); + setPlateNumbers(plates); + setProjects(custs); + }).catch(() => {}); + }, []); + + // 加载数据(带服务端筛选/排序/分页) + useEffect(() => { + const load = () => { + fetchMonitoring({ + sortBy, + sortOrder, + limit: 100, + search: searchTerm || undefined, + dept: filterDept !== 'All' ? filterDept : undefined, + customer: filterProject !== 'All' ? filterProject : undefined, + }).then(d => { + setFilteredVehicles(d.vehicles); + setTotalCount(d.total); + }).catch(() => {}); + }; load(); const interval = setInterval(load, 60000); return () => clearInterval(interval); - }, []); + }, [sortBy, sortOrder, searchTerm, filterDept, filterProject]); const toggleFullscreen = () => { if (!isFullscreen) { @@ -129,34 +158,6 @@ export default function MonitoringView() { setIsFullscreen(!isFullscreen); }; - const filteredVehicles = useMemo(() => { - return allVehicles.filter(v => { - const matchesSearch = v.plate.toLowerCase().includes(searchTerm.toLowerCase()) || - (v.customer || '').toLowerCase().includes(searchTerm.toLowerCase()); - const matchesDept = filterDept === 'All' || v.department === filterDept; - const matchesPlate = filterPlate === 'All' || v.plate === filterPlate; - const matchesProject = filterProject === 'All' || v.customer === filterProject; - const matchesEntity = filterEntity === 'All' || true; - const matchesRegion = filterRegionCode === 'All' || true; - - // Mileage range filter - const mileage = v.dailyKm || 0; - const minMileage = filterMileageRange.min === '' ? -Infinity : Number(filterMileageRange.min); - const maxMileage = filterMileageRange.max === '' ? Infinity : Number(filterMileageRange.max); - const matchesMileage = mileage >= minMileage && mileage <= maxMileage; - - return matchesSearch && matchesDept && matchesPlate && matchesProject && matchesEntity && matchesRegion && matchesMileage; - }).sort((a, b) => { - const valA = sortBy === 'today' ? (a.dailyKm || 0) : (a.totalKm || 0); - const valB = sortBy === 'today' ? (b.dailyKm || 0) : (b.totalKm || 0); - return sortOrder === 'desc' ? valB - valA : valA - valB; - }); - }, [allVehicles, searchTerm, filterDept, sortBy, sortOrder, filterPlate, filterProject, filterEntity, filterRegionCode, filterMileageRange]); - - const departments = Array.from(new Set(allVehicles.map(v => v.department).filter(Boolean))) as string[]; - const plateNumbers = Array.from(new Set(allVehicles.map(v => v.plate).filter(Boolean))); - const projects = Array.from(new Set(allVehicles.map(v => v.customer).filter(Boolean))) as string[]; - const stats = useMemo(() => { const totalToday = filteredVehicles.reduce((sum, v) => sum + (v.dailyKm || 0), 0); const totalAll = filteredVehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0); @@ -164,8 +165,8 @@ export default function MonitoringView() { const activeTotal = sortBy === 'today' ? totalToday : totalAll; const activeAvg = filteredVehicles.length > 0 ? activeTotal / filteredVehicles.length : 0; - return { totalToday, totalAll, activeTotal, activeAvg, count: filteredVehicles.length }; - }, [filteredVehicles, sortBy]); + return { totalToday, totalAll, activeTotal, activeAvg, count: totalCount }; + }, [filteredVehicles, sortBy, totalCount]); return ( <> diff --git a/src/modules/mileage/api.ts b/src/modules/mileage/api.ts index 986c433..934121d 100644 --- a/src/modules/mileage/api.ts +++ b/src/modules/mileage/api.ts @@ -8,8 +8,25 @@ async function fetchJson(url: string): Promise { return res.json(); } -export async function fetchMonitoring(): Promise { - return fetchJson(`${BASE}/monitoring`); +export async function fetchMonitoring(params?: { + sortBy?: string; + sortOrder?: string; + limit?: number; + offset?: number; + search?: string; + dept?: string; + customer?: string; +}): Promise { + const query = new URLSearchParams(); + if (params?.sortBy) query.set('sortBy', params.sortBy); + if (params?.sortOrder) query.set('sortOrder', params.sortOrder); + if (params?.limit) query.set('limit', String(params.limit)); + if (params?.offset) query.set('offset', String(params.offset)); + if (params?.search) query.set('search', params.search); + if (params?.dept) query.set('dept', params.dept); + if (params?.customer) query.set('customer', params.customer); + const qs = query.toString(); + return fetchJson(`${BASE}/monitoring${qs ? `?${qs}` : ''}`); } export async function fetchTargets(): Promise { diff --git a/src/modules/mileage/types.ts b/src/modules/mileage/types.ts index 589b0d1..ed6887c 100644 --- a/src/modules/mileage/types.ts +++ b/src/modules/mileage/types.ts @@ -13,6 +13,7 @@ export interface MonitoringVehicle { export interface MonitoringData { vehicles: MonitoringVehicle[]; + total: number; updatedAt: string; } diff --git a/src/server/routes/mileage.ts b/src/server/routes/mileage.ts index 3c74fc1..3fbeee1 100644 --- a/src/server/routes/mileage.ts +++ b/src/server/routes/mileage.ts @@ -18,41 +18,64 @@ LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0 LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 WHERE truck.is_deleted = 0 AND truck.is_operation = 1`; +// 车辆关联信息缓存(5分钟过期) +let vehicleInfoCache: { data: Map; expiry: number } | null = null; +const CACHE_TTL = 5 * 60 * 1000; + +async function getVehicleInfoMap(): Promise> { + const now = Date.now(); + if (vehicleInfoCache && now < vehicleInfoCache.expiry) { + return vehicleInfoCache.data; + } + const [rows] = await pool.execute(VEHICLE_INFO_SQL) as any; + const map = new Map(); + for (const row of rows) { + map.set(row.plate, row); + } + vehicleInfoCache = { data: map, expiry: now + CACHE_TTL }; + return map; +} + // GET /monitoring — 实时监控数据 app.get('/monitoring', async (c) => { try { - // 1. 从 hydrogen_energy 取最新日期的里程数据 - const [dateRows] = await mileagePool.execute( - 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' - ) as any; - const latestDate = dateRows[0]?.latest; - if (!latestDate) return c.json({ vehicles: [], updatedAt: new Date().toISOString() }); + const sortBy = c.req.query('sortBy') || 'today'; + const sortOrder = c.req.query('sortOrder') || 'desc'; + const limit = Number(c.req.query('limit')) || 100; + const offset = Number(c.req.query('offset')) || 0; + const search = c.req.query('search') || ''; + const dept = c.req.query('dept') || ''; + const customer = c.req.query('customer') || ''; - const [mileageRows] = await mileagePool.execute( - `SELECT plate, vin, daily_km, total_km, source - FROM v_vehicle_daily_stats - WHERE stat_date = ?`, - [latestDate] - ) as any; + // 并行查询两个数据库 + const [mileageResult, infoMap] = await Promise.all([ + (async () => { + const [dateRows] = await mileagePool.execute( + 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' + ) as any; + const latestDate = dateRows[0]?.latest; + if (!latestDate) return []; + const [rows] = await mileagePool.execute( + `SELECT plate, vin, daily_km, total_km, source + FROM v_vehicle_daily_stats WHERE stat_date = ?`, + [latestDate] + ) as any; + return rows; + })(), + getVehicleInfoMap(), + ]); - // 对于同一 plate 可能有多条记录(不同 source),取 daily_km 最大的 + // 去重:同一 plate 取 daily_km 最大的 const mileageMap = new Map(); - for (const row of mileageRows) { + for (const row of mileageResult) { const existing = mileageMap.get(row.plate); if (!existing || Number(row.daily_km) > Number(existing.daily_km)) { mileageMap.set(row.plate, row); } } - // 2. 从 lingniu_prod 取车辆关联信息 - const [infoRows] = await pool.execute(VEHICLE_INFO_SQL) as any; - const infoMap = new Map(); - for (const row of infoRows) { - infoMap.set(row.plate, row); - } - - // 3. 合并 - const vehicles = Array.from(mileageMap.values()).map((m: any) => { + // 合并 + 筛选 + let vehicles = 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'; @@ -70,10 +93,33 @@ app.get('/monitoring', async (c) => { }; }); - return c.json({ vehicles, updatedAt: new Date().toISOString() }); + // 服务端筛选 + if (search) { + const q = search.toLowerCase(); + vehicles = vehicles.filter(v => + v.plate.toLowerCase().includes(q) || + (v.customer || '').toLowerCase().includes(q) + ); + } + if (dept) vehicles = vehicles.filter(v => v.department === dept); + if (customer) vehicles = vehicles.filter(v => v.customer === customer); + + const total = vehicles.length; + + // 服务端排序 + vehicles.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 paged = vehicles.slice(offset, offset + limit); + + return c.json({ vehicles: paged, total, updatedAt: new Date().toISOString() }); } catch (e) { console.error('monitoring error:', e); - return c.json({ vehicles: [], updatedAt: new Date().toISOString() }, 500); + return c.json({ vehicles: [], total: 0, updatedAt: new Date().toISOString() }, 500); } });