All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 后端: 选中 region 时基于该区域车辆重算 filters,车牌列表只展示该区域 - 前端: filterOptions.plates 收窄后自动从已选车牌中剔除不属于新区域的项 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { getCache, queryDateMileage, 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;
|
|
targetName: 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.targetName) {
|
|
const cache = getCache();
|
|
const tPlates = cache?.targetPlatesMap.get(params.targetName);
|
|
result = tPlates ? result.filter(v => tPlates.has(v.plate)) : [];
|
|
}
|
|
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;
|
|
}
|
|
|
|
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 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') || '',
|
|
targetName: c.req.query('targetName') || '',
|
|
region: c.req.query('region') || '',
|
|
mileageMin: c.req.query('mileageMin') || '',
|
|
mileageMax: c.req.query('mileageMax') || '',
|
|
};
|
|
|
|
let allVehicles: CachedVehicle[];
|
|
let filters: MonitoringFilters;
|
|
|
|
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);
|
|
|
|
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,
|
|
total,
|
|
page,
|
|
totalPages: Math.ceil(total / limit),
|
|
updatedAt: date || getCache()?.updatedAt || new Date().toISOString(),
|
|
});
|
|
});
|
|
|
|
export default app;
|