diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 0104bfe..3838225 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -1,11 +1,11 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Truck, Search, Filter, ChevronDown, Maximize2, Minimize2, RotateCcw, - ArrowUp, ArrowDown, + ArrowUp, ArrowDown, ChevronLeft, ChevronRight, } from 'lucide-react'; -import type { MonitoringVehicle } from './types'; +import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types'; import { fetchMonitoring } from './api'; const SearchableSelect = ({ @@ -63,7 +63,7 @@ const SearchableSelect = ({ > 无限制 - {filtered.map(opt => ( + {filtered.map((opt: string) => (
([]); - const [totalCount, setTotalCount] = useState(0); - const [departments, setDepartments] = useState([]); - const [plateNumbers, setPlateNumbers] = useState([]); - const [projects, setProjects] = useState([]); + const [vehicles, setVehicles] = useState([]); + const [stats, setStats] = useState({ totalToday: 0, totalAll: 0, onlineCount: 0, vehicleCount: 0 }); + const [filterOptions, setFilterOptions] = useState({ departments: [], customers: [], plates: [] }); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [updatedAt, setUpdatedAt] = useState(''); + const PAGE_SIZE = 50; - // 加载筛选选项(仅首次,用轻量请求) - useEffect(() => { - 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); + const departments = filterOptions.departments; + const plateNumbers = filterOptions.plates; + const projects = filterOptions.customers; + + const loadData = useCallback((p = page) => { + fetchMonitoring({ + sortBy, + sortOrder, + limit: PAGE_SIZE, + page: p, + search: searchTerm || undefined, + dept: filterDept !== 'All' ? filterDept : undefined, + customer: filterProject !== 'All' ? filterProject : undefined, + }).then(d => { + setVehicles(d.vehicles); + setStats(d.stats); + setFilterOptions(d.filters); + setTotal(d.total); + setPage(d.page); + setTotalPages(d.totalPages); + setUpdatedAt(d.updatedAt); }).catch(() => {}); - }, []); + }, [sortBy, sortOrder, searchTerm, filterDept, filterProject, page]); - // 加载数据(带服务端筛选/排序/分页) + // 筛选/排序变化时重置到第1页 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); + setPage(1); + loadData(1); }, [sortBy, sortOrder, searchTerm, filterDept, filterProject]); + // 翻页时加载 + useEffect(() => { + loadData(page); + }, [page]); + + const filteredVehicles = vehicles; + const toggleFullscreen = () => { if (!isFullscreen) { const elem = document.documentElement; @@ -158,16 +166,6 @@ export default function MonitoringView() { setIsFullscreen(!isFullscreen); }; - 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); - - const activeTotal = sortBy === 'today' ? totalToday : totalAll; - const activeAvg = filteredVehicles.length > 0 ? activeTotal / filteredVehicles.length : 0; - - return { totalToday, totalAll, activeTotal, activeAvg, count: totalCount }; - }, [filteredVehicles, sortBy, totalCount]); - return ( <> {/* Fullscreen Landscape View Overlay */} @@ -227,7 +225,7 @@ export default function MonitoringView() {
监控台数
- {stats.count} + {stats.vehicleCount}
@@ -236,7 +234,7 @@ export default function MonitoringView() { 平均单车 ({sortBy === 'today' ? '今日' : '累计'})
- {stats.activeAvg.toFixed(0)} + {(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)} KM
@@ -687,19 +685,19 @@ export default function MonitoringView() { {sortBy === 'today' ? '今日' : '累计'}总里程 (KM)
- {stats.activeTotal.toLocaleString()} + {(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} {sortBy === 'today' && {'\u2191'}12%}
平均单车
-
{stats.activeAvg.toFixed(0)}
+
{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}
KM/台
监控台数
-
{stats.count}
+
{stats.vehicleCount}
@@ -769,6 +767,29 @@ export default function MonitoringView() {

未找到匹配数据

)} + + {/* 分页控件 */} + {totalPages > 1 && ( +
+ + + {page} / {totalPages} + + +
+ )} ); diff --git a/src/modules/mileage/api.ts b/src/modules/mileage/api.ts index 934121d..558331b 100644 --- a/src/modules/mileage/api.ts +++ b/src/modules/mileage/api.ts @@ -12,7 +12,7 @@ export async function fetchMonitoring(params?: { sortBy?: string; sortOrder?: string; limit?: number; - offset?: number; + page?: number; search?: string; dept?: string; customer?: string; @@ -21,7 +21,7 @@ export async function fetchMonitoring(params?: { 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?.page) query.set('page', String(params.page)); if (params?.search) query.set('search', params.search); if (params?.dept) query.set('dept', params.dept); if (params?.customer) query.set('customer', params.customer); diff --git a/src/modules/mileage/types.ts b/src/modules/mileage/types.ts index ed6887c..00a7ff1 100644 --- a/src/modules/mileage/types.ts +++ b/src/modules/mileage/types.ts @@ -11,9 +11,26 @@ export interface MonitoringVehicle { manager: string | null; } +export interface MonitoringStats { + totalToday: number; + totalAll: number; + onlineCount: number; + vehicleCount: number; +} + +export interface MonitoringFilters { + departments: string[]; + customers: string[]; + plates: string[]; +} + export interface MonitoringData { vehicles: MonitoringVehicle[]; + stats: MonitoringStats; + filters: MonitoringFilters; total: number; + page: number; + totalPages: number; updatedAt: string; } diff --git a/src/server/routes/mileage.ts b/src/server/routes/mileage.ts index 3fbeee1..6252869 100644 --- a/src/server/routes/mileage.ts +++ b/src/server/routes/mileage.ts @@ -18,37 +18,36 @@ 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; +// ========== 实时监控缓存(每2分钟刷新) ========== +interface CachedVehicle { + plate: string; + vin: string; + dailyKm: number; + totalKm: number | null; + source: string; + isOnline: boolean; + isDataSynced: boolean; + customer: string | null; + department: string | null; + manager: string | null; } -// GET /monitoring — 实时监控数据 -app.get('/monitoring', async (c) => { +interface MonitoringCache { + vehicles: CachedVehicle[]; + stats: { totalToday: number; totalAll: number; onlineCount: number; vehicleCount: number }; + filters: { departments: string[]; customers: string[]; plates: string[] }; + updatedAt: string; +} + +let monitoringCache: MonitoringCache | null = null; + +async function refreshMonitoringCache() { try { - 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') || ''; + console.log('[mileage] refreshing monitoring cache...'); + const start = Date.now(); // 并行查询两个数据库 - const [mileageResult, infoMap] = await Promise.all([ + const [mileageResult, infoRows] = await Promise.all([ (async () => { const [dateRows] = await mileagePool.execute( 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' @@ -62,9 +61,15 @@ app.get('/monitoring', async (c) => { ) as any; return rows; })(), - getVehicleInfoMap(), + pool.execute(VEHICLE_INFO_SQL).then(([rows]) => rows as any[]), ]); + // 车辆关联信息 map + const infoMap = new Map(); + for (const row of infoRows) { + infoMap.set(row.plate, row); + } + // 去重:同一 plate 取 daily_km 最大的 const mileageMap = new Map(); for (const row of mileageResult) { @@ -74,8 +79,8 @@ app.get('/monitoring', async (c) => { } } - // 合并 + 筛选 - let vehicles = Array.from(mileageMap.values()).map((m: any) => { + // 合并 + 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'; @@ -93,34 +98,82 @@ app.get('/monitoring', async (c) => { }; }); - // 服务端筛选 - 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 totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0); + const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0); + const onlineCount = vehicles.filter(v => v.isOnline).length; - const total = vehicles.length; + // 预提取筛选选项 + const departments = Array.from(new Set(vehicles.map(v => v.department).filter(Boolean))) as string[]; + const customers = Array.from(new Set(vehicles.map(v => v.customer).filter(Boolean))) as string[]; + const plates = vehicles.map(v => v.plate); - // 服务端排序 - 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; - }); + monitoringCache = { + vehicles, + stats: { totalToday, totalAll, onlineCount, vehicleCount: vehicles.length }, + filters: { departments, customers, plates }, + updatedAt: new Date().toISOString(), + }; - // 分页 - const paged = vehicles.slice(offset, offset + limit); - - return c.json({ vehicles: paged, total, updatedAt: new Date().toISOString() }); + console.log(`[mileage] cache refreshed: ${vehicles.length} vehicles in ${Date.now() - start}ms`); } catch (e) { - console.error('monitoring error:', e); - return c.json({ vehicles: [], total: 0, updatedAt: new Date().toISOString() }, 500); + console.error('[mileage] cache refresh error:', e); } +} + +// 启动时立即刷新,之后每2分钟刷新 +refreshMonitoringCache(); +setInterval(refreshMonitoringCache, 2 * 60 * 1000); + +// GET /monitoring — 从缓存取数据,支持筛选/排序/分页 +app.get('/monitoring', (c) => { + if (!monitoringCache) { + return c.json({ vehicles: [], stats: { totalToday: 0, totalAll: 0, onlineCount: 0, vehicleCount: 0 }, filters: { departments: [], customers: [], plates: [] }, total: 0, 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')) || 50; + const page = Number(c.req.query('page')) || 1; + const search = c.req.query('search') || ''; + const dept = c.req.query('dept') || ''; + const customer = c.req.query('customer') || ''; + + let vehicles = monitoringCache.vehicles; + + // 筛选 + 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 = [...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 offset = (page - 1) * limit; + const paged = vehicles.slice(offset, offset + limit); + + return c.json({ + vehicles: paged, + stats: monitoringCache.stats, + filters: monitoringCache.filters, + total, + page, + totalPages: Math.ceil(total / limit), + updatedAt: monitoringCache.updatedAt, + }); }); // GET /targets — 考核项目列表 + 汇总