feat(mileage): add brand multi-select filter and customer fuzzy search
ci/woodpecker/push/woodpecker Pipeline was successful

- Add brand multi-select filter (BatchMultiSelect) with 17 brand options
- Replace customer select with SearchableSelect for fuzzy search
- Server-side: add brand to vehicle-info SQL, cache, filters, and monitoring
- Customer filter now uses case-insensitive includes match on server side
- Add brand column to fullscreen table header and body
- Mobile-adaptive: compact inputs, constrained widths, scrollable dropdowns
- Bump version to 1.1.8
This commit is contained in:
kkfluous
2026-07-15 14:25:11 +08:00
parent ae4b5d30e7
commit 2b31f8efe8
8 changed files with 127 additions and 29 deletions
+3 -1
View File
@@ -37,6 +37,7 @@ function buildFilters(vehicles: CachedVehicle[], targetNames: string[]): Monitor
const projects = Array.from(new Set(vehicles.map(v => v.project).filter((p): p is string => p !== null)));
const entities = Array.from(new Set(vehicles.map(v => v.entity).filter((e): e is string => e !== null)));
const rentStatuses = Array.from(new Set(vehicles.map(v => v.rentStatus).filter((r): r is string => r !== null)));
const brands = Array.from(new Set(vehicles.map(v => v.brand).filter((b): b is string => b !== null))).sort();
const prefixCount = new Map<string, number>();
for (const v of vehicles) {
@@ -54,7 +55,7 @@ function buildFilters(vehicles: CachedVehicle[], targetNames: string[]): Monitor
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
});
return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames, regions };
return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames, regions, brands };
}
interface MileageRow {
@@ -199,6 +200,7 @@ function mergeVehicles(
entity: info.entity || null,
project: info.project || null,
region: regionMap[plate] || null,
brand: info.brand_label || null,
targetNames: targetNamesByPlate.get(plate) || [],
yesterdayKm: yesterdayMap.get(plate) || 0,
};
+8 -2
View File
@@ -9,7 +9,7 @@ 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: [] },
filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [], brands: [] },
total: 0,
page: 1,
totalPages: 1,
@@ -20,6 +20,7 @@ function applyFilters(vehicles: CachedVehicle[], params: {
search: string; dept: string; customer: string; project: string;
entity: string; rentStatus: string; plate: string; platePrefix: string;
targetNames: string[]; region: string; mileageMin: string; mileageMax: string;
brands: string[];
}): CachedVehicle[] {
let result = vehicles;
@@ -32,7 +33,7 @@ function applyFilters(vehicles: CachedVehicle[], params: {
);
}
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.customer) result = result.filter(v => params.customer === '__EMPTY__' ? !v.customer : (v.customer || '').toLowerCase().includes(params.customer.toLowerCase()));
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);
@@ -46,6 +47,10 @@ function applyFilters(vehicles: CachedVehicle[], params: {
const selectedTargets = new Set(params.targetNames);
result = result.filter(v => v.targetNames.some(targetName => selectedTargets.has(targetName)));
}
if (params.brands.length > 0) {
const selectedBrands = new Set(params.brands);
result = result.filter(v => v.brand != null && selectedBrands.has(v.brand));
}
if (params.mileageMin) result = result.filter(v => v.dailyKm >= Number(params.mileageMin));
if (params.mileageMax) result = result.filter(v => v.dailyKm <= Number(params.mileageMax));
@@ -112,6 +117,7 @@ app.get('/', async (c) => {
region: c.req.query('region') || '',
mileageMin: c.req.query('mileageMin') || '',
mileageMax: c.req.query('mileageMax') || '',
brands: c.req.queries('brand') || [],
};
let allVehicles: CachedVehicle[];
+3
View File
@@ -16,6 +16,7 @@ export interface CachedVehicle {
entity: string | null;
project: string | null;
region: string | null;
brand: string | null;
targetNames: string[];
yesterdayKm: number;
}
@@ -37,6 +38,7 @@ export interface MonitoringFilters {
platePrefixes: PlatePrefix[];
targetNames: string[];
regions: string[];
brands: string[];
}
/** 监控缓存 */
@@ -80,4 +82,5 @@ export interface VehicleInfoRow {
rent_status: string | null;
entity: string | null;
project: string | null;
brand_label: string | null;
}
+25 -2
View File
@@ -1,7 +1,7 @@
import pool from '../../db.js';
import type { VehicleInfoRow } from './types.js';
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目) */
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌 */
export const VEHICLE_INFO_SQL = `SELECT
vi.plate_number AS plate,
vi.vin AS vin,
@@ -18,11 +18,34 @@ export const VEHICLE_INFO_SQL = `SELECT
ELSE vs.operation_status
END AS rent_status,
NULLIF(vi.registered_ownership, '') AS entity,
COALESCE(c.project_name, vor.project_name) AS project
COALESCE(c.project_name, vor.project_name) AS project,
CASE vm.brand
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
WHEN 'yuejin' THEN '跃进'
WHEN 'feichi' THEN '飞驰'
WHEN 'sulong' THEN '苏龙'
WHEN 'higer' THEN '海格'
WHEN 'dongfeng' THEN '东风'
WHEN 'yutong' THEN '宇通'
WHEN 'chufeng' THEN '楚风'
WHEN 'tonghua' THEN '通华'
WHEN 'maxus' THEN '大通'
WHEN 'mingwei' THEN '明威'
WHEN 'wanfeng' THEN '万风'
WHEN 'shujie' THEN '舒捷'
WHEN 'denza' THEN '腾势'
WHEN 'hongyan' THEN '红岩'
WHEN 'yuanchang brand' THEN '远程牌'
WHEN 'others' THEN '其他'
ELSE vm.brand
END AS brand_label
FROM vehicle_info vi
LEFT JOIN vehicle_status vs
ON vs.vehicle_id = vi.id
AND vs.del_flag = 0
LEFT JOIN vehicle_model vm
ON vm.id = vi.vehicle_model_id
AND vm.del_flag = '0'
LEFT JOIN vehicle_lease_order_record vor
ON vor.vehicle_id = vi.id
AND vor.del_flag = '0'