perf: 实时监控改为缓存+分页架构
后端: - 每2分钟刷新全量数据到内存缓存(并行查询两库) - 预计算统计信息(totalToday/totalAll/onlineCount/vehicleCount) - 预提取筛选选项(departments/customers/plates) - API 直接从缓存读取,支持分页(每页50条)+筛选+排序 前端: - KPI 统计使用后端返回的 stats - 车辆列表分页,带翻页控件 - 筛选选项从后端 filters 获取 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import {
|
import {
|
||||||
Truck, Search, Filter, ChevronDown,
|
Truck, Search, Filter, ChevronDown,
|
||||||
Maximize2, Minimize2, RotateCcw,
|
Maximize2, Minimize2, RotateCcw,
|
||||||
ArrowUp, ArrowDown,
|
ArrowUp, ArrowDown, ChevronLeft, ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { MonitoringVehicle } from './types';
|
import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types';
|
||||||
import { fetchMonitoring } from './api';
|
import { fetchMonitoring } from './api';
|
||||||
|
|
||||||
const SearchableSelect = ({
|
const SearchableSelect = ({
|
||||||
@@ -63,7 +63,7 @@ const SearchableSelect = ({
|
|||||||
>
|
>
|
||||||
无限制
|
无限制
|
||||||
</div>
|
</div>
|
||||||
{filtered.map(opt => (
|
{filtered.map((opt: string) => (
|
||||||
<div
|
<div
|
||||||
key={opt}
|
key={opt}
|
||||||
className="px-3 py-2 text-[10px] font-bold text-slate-600 hover:bg-slate-50 cursor-pointer border-t border-slate-50"
|
className="px-3 py-2 text-[10px] font-bold text-slate-600 hover:bg-slate-50 cursor-pointer border-t border-slate-50"
|
||||||
@@ -106,44 +106,52 @@ export default function MonitoringView() {
|
|||||||
const [filterRegionCode, setFilterRegionCode] = useState('All');
|
const [filterRegionCode, setFilterRegionCode] = useState('All');
|
||||||
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
|
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
|
||||||
|
|
||||||
const [filteredVehicles, setFilteredVehicles] = useState<MonitoringVehicle[]>([]);
|
const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, onlineCount: 0, vehicleCount: 0 });
|
||||||
const [departments, setDepartments] = useState<string[]>([]);
|
const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [] });
|
||||||
const [plateNumbers, setPlateNumbers] = useState<string[]>([]);
|
const [total, setTotal] = useState(0);
|
||||||
const [projects, setProjects] = useState<string[]>([]);
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
const [updatedAt, setUpdatedAt] = useState('');
|
||||||
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
// 加载筛选选项(仅首次,用轻量请求)
|
const departments = filterOptions.departments;
|
||||||
useEffect(() => {
|
const plateNumbers = filterOptions.plates;
|
||||||
fetchMonitoring({ limit: 2000 }).then(d => {
|
const projects = filterOptions.customers;
|
||||||
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 loadData = useCallback((p = page) => {
|
||||||
const custs = Array.from(new Set(d.vehicles.map(v => v.customer).filter(Boolean))) as string[];
|
fetchMonitoring({
|
||||||
setDepartments(depts);
|
sortBy,
|
||||||
setPlateNumbers(plates);
|
sortOrder,
|
||||||
setProjects(custs);
|
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(() => {});
|
}).catch(() => {});
|
||||||
}, []);
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject, page]);
|
||||||
|
|
||||||
// 加载数据(带服务端筛选/排序/分页)
|
// 筛选/排序变化时重置到第1页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = () => {
|
setPage(1);
|
||||||
fetchMonitoring({
|
loadData(1);
|
||||||
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]);
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject]);
|
||||||
|
|
||||||
|
// 翻页时加载
|
||||||
|
useEffect(() => {
|
||||||
|
loadData(page);
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
const filteredVehicles = vehicles;
|
||||||
|
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
const elem = document.documentElement;
|
const elem = document.documentElement;
|
||||||
@@ -158,16 +166,6 @@ export default function MonitoringView() {
|
|||||||
setIsFullscreen(!isFullscreen);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Fullscreen Landscape View Overlay */}
|
{/* Fullscreen Landscape View Overlay */}
|
||||||
@@ -227,7 +225,7 @@ export default function MonitoringView() {
|
|||||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">监控台数</div>
|
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">监控台数</div>
|
||||||
<div className="text-2xl font-black text-white tracking-tighter">
|
<div className="text-2xl font-black text-white tracking-tighter">
|
||||||
{stats.count}
|
{stats.vehicleCount}
|
||||||
<span className="text-blue-400 text-xs ml-2">台</span>
|
<span className="text-blue-400 text-xs ml-2">台</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +234,7 @@ export default function MonitoringView() {
|
|||||||
平均单车 ({sortBy === 'today' ? '今日' : '累计'})
|
平均单车 ({sortBy === 'today' ? '今日' : '累计'})
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-black text-white tracking-tighter">
|
<div className="text-2xl font-black text-white tracking-tighter">
|
||||||
{stats.activeAvg.toFixed(0)}
|
{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}
|
||||||
<span className="text-blue-400 text-xs ml-2">KM</span>
|
<span className="text-blue-400 text-xs ml-2">KM</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -687,19 +685,19 @@ export default function MonitoringView() {
|
|||||||
{sortBy === 'today' ? '今日' : '累计'}总里程 (KM)
|
{sortBy === 'today' ? '今日' : '累计'}总里程 (KM)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-black tracking-tighter flex items-baseline gap-1">
|
<div className="text-2xl font-black tracking-tighter flex items-baseline gap-1">
|
||||||
{stats.activeTotal.toLocaleString()}
|
{(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()}
|
||||||
{sortBy === 'today' && <span className="text-blue-400 text-[10px] font-bold">{'\u2191'}12%</span>}
|
{sortBy === 'today' && <span className="text-blue-400 text-[10px] font-bold">{'\u2191'}12%</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -right-4 -bottom-4 w-12 h-12 bg-blue-500/10 rounded-full blur-xl"></div>
|
<div className="absolute -right-4 -bottom-4 w-12 h-12 bg-blue-500/10 rounded-full blur-xl"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-3 rounded-2xl border border-gray-100 shadow-sm">
|
<div className="bg-white p-3 rounded-2xl border border-gray-100 shadow-sm">
|
||||||
<div className="text-[8px] font-bold text-slate-400 uppercase mb-1">平均单车</div>
|
<div className="text-[8px] font-bold text-slate-400 uppercase mb-1">平均单车</div>
|
||||||
<div className="text-sm font-black text-slate-800">{stats.activeAvg.toFixed(0)}</div>
|
<div className="text-sm font-black text-slate-800">{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}</div>
|
||||||
<div className="text-[7px] text-slate-400 mt-0.5">KM/台</div>
|
<div className="text-[7px] text-slate-400 mt-0.5">KM/台</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-3 rounded-2xl border border-gray-100 shadow-sm">
|
<div className="bg-white p-3 rounded-2xl border border-gray-100 shadow-sm">
|
||||||
<div className="text-[8px] font-bold text-slate-400 uppercase mb-1">监控台数</div>
|
<div className="text-[8px] font-bold text-slate-400 uppercase mb-1">监控台数</div>
|
||||||
<div className="text-sm font-black text-slate-800">{stats.count}</div>
|
<div className="text-sm font-black text-slate-800">{stats.vehicleCount}</div>
|
||||||
<div className="text-[7px] text-slate-400 mt-0.5">台</div>
|
<div className="text-[7px] text-slate-400 mt-0.5">台</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -769,6 +767,29 @@ export default function MonitoringView() {
|
|||||||
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 分页控件 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-3 py-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(Math.max(1, page - 1))}
|
||||||
|
disabled={page <= 1}
|
||||||
|
className="p-1.5 rounded-lg bg-white border border-slate-100 shadow-sm disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={14} className="text-slate-600" />
|
||||||
|
</button>
|
||||||
|
<span className="text-[10px] font-bold text-slate-400">
|
||||||
|
{page} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
className="p-1.5 rounded-lg bg-white border border-slate-100 shadow-sm disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={14} className="text-slate-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export async function fetchMonitoring(params?: {
|
|||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: string;
|
sortOrder?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
page?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
dept?: string;
|
dept?: string;
|
||||||
customer?: string;
|
customer?: string;
|
||||||
@@ -21,7 +21,7 @@ export async function fetchMonitoring(params?: {
|
|||||||
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
||||||
if (params?.sortOrder) query.set('sortOrder', params.sortOrder);
|
if (params?.sortOrder) query.set('sortOrder', params.sortOrder);
|
||||||
if (params?.limit) query.set('limit', String(params.limit));
|
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?.search) query.set('search', params.search);
|
||||||
if (params?.dept) query.set('dept', params.dept);
|
if (params?.dept) query.set('dept', params.dept);
|
||||||
if (params?.customer) query.set('customer', params.customer);
|
if (params?.customer) query.set('customer', params.customer);
|
||||||
|
|||||||
@@ -11,9 +11,26 @@ export interface MonitoringVehicle {
|
|||||||
manager: string | null;
|
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 {
|
export interface MonitoringData {
|
||||||
vehicles: MonitoringVehicle[];
|
vehicles: MonitoringVehicle[];
|
||||||
|
stats: MonitoringStats;
|
||||||
|
filters: MonitoringFilters;
|
||||||
total: number;
|
total: number;
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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`;
|
WHERE truck.is_deleted = 0 AND truck.is_operation = 1`;
|
||||||
|
|
||||||
// 车辆关联信息缓存(5分钟过期)
|
// ========== 实时监控缓存(每2分钟刷新) ==========
|
||||||
let vehicleInfoCache: { data: Map<string, any>; expiry: number } | null = null;
|
interface CachedVehicle {
|
||||||
const CACHE_TTL = 5 * 60 * 1000;
|
plate: string;
|
||||||
|
vin: string;
|
||||||
async function getVehicleInfoMap(): Promise<Map<string, any>> {
|
dailyKm: number;
|
||||||
const now = Date.now();
|
totalKm: number | null;
|
||||||
if (vehicleInfoCache && now < vehicleInfoCache.expiry) {
|
source: string;
|
||||||
return vehicleInfoCache.data;
|
isOnline: boolean;
|
||||||
}
|
isDataSynced: boolean;
|
||||||
const [rows] = await pool.execute(VEHICLE_INFO_SQL) as any;
|
customer: string | null;
|
||||||
const map = new Map<string, any>();
|
department: string | null;
|
||||||
for (const row of rows) {
|
manager: string | null;
|
||||||
map.set(row.plate, row);
|
|
||||||
}
|
|
||||||
vehicleInfoCache = { data: map, expiry: now + CACHE_TTL };
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /monitoring — 实时监控数据
|
interface MonitoringCache {
|
||||||
app.get('/monitoring', async (c) => {
|
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 {
|
try {
|
||||||
const sortBy = c.req.query('sortBy') || 'today';
|
console.log('[mileage] refreshing monitoring cache...');
|
||||||
const sortOrder = c.req.query('sortOrder') || 'desc';
|
const start = Date.now();
|
||||||
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 [mileageResult, infoMap] = await Promise.all([
|
const [mileageResult, infoRows] = await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
const [dateRows] = await mileagePool.execute(
|
const [dateRows] = await mileagePool.execute(
|
||||||
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
|
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
|
||||||
@@ -62,9 +61,15 @@ app.get('/monitoring', async (c) => {
|
|||||||
) as any;
|
) as any;
|
||||||
return rows;
|
return rows;
|
||||||
})(),
|
})(),
|
||||||
getVehicleInfoMap(),
|
pool.execute(VEHICLE_INFO_SQL).then(([rows]) => rows as any[]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 车辆关联信息 map
|
||||||
|
const infoMap = new Map<string, any>();
|
||||||
|
for (const row of infoRows) {
|
||||||
|
infoMap.set(row.plate, row);
|
||||||
|
}
|
||||||
|
|
||||||
// 去重:同一 plate 取 daily_km 最大的
|
// 去重:同一 plate 取 daily_km 最大的
|
||||||
const mileageMap = new Map<string, any>();
|
const mileageMap = new Map<string, any>();
|
||||||
for (const row of mileageResult) {
|
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 info = infoMap.get(m.plate);
|
||||||
const dailyKm = Number(m.daily_km) || 0;
|
const dailyKm = Number(m.daily_km) || 0;
|
||||||
const source = m.source || 'NONE';
|
const source = m.source || 'NONE';
|
||||||
@@ -93,34 +98,82 @@ app.get('/monitoring', async (c) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 服务端筛选
|
// 预计算统计信息
|
||||||
if (search) {
|
const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0);
|
||||||
const q = search.toLowerCase();
|
const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
|
||||||
vehicles = vehicles.filter(v =>
|
const onlineCount = vehicles.filter(v => v.isOnline).length;
|
||||||
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;
|
// 预提取筛选选项
|
||||||
|
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);
|
||||||
|
|
||||||
// 服务端排序
|
monitoringCache = {
|
||||||
vehicles.sort((a, b) => {
|
vehicles,
|
||||||
const valA = sortBy === 'today' ? a.dailyKm : (a.totalKm || 0);
|
stats: { totalToday, totalAll, onlineCount, vehicleCount: vehicles.length },
|
||||||
const valB = sortBy === 'today' ? b.dailyKm : (b.totalKm || 0);
|
filters: { departments, customers, plates },
|
||||||
return sortOrder === 'desc' ? valB - valA : valA - valB;
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
};
|
||||||
|
|
||||||
// 分页
|
console.log(`[mileage] cache refreshed: ${vehicles.length} vehicles in ${Date.now() - start}ms`);
|
||||||
const paged = vehicles.slice(offset, offset + limit);
|
|
||||||
|
|
||||||
return c.json({ vehicles: paged, total, updatedAt: new Date().toISOString() });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('monitoring error:', e);
|
console.error('[mileage] cache refresh error:', e);
|
||||||
return c.json({ vehicles: [], total: 0, updatedAt: new Date().toISOString() }, 500);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动时立即刷新,之后每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 — 考核项目列表 + 汇总
|
// GET /targets — 考核项目列表 + 汇总
|
||||||
|
|||||||
Reference in New Issue
Block a user