feat(mileage): 外部三选筛选 + 车牌多选粘贴 + 运营区域 + xlsx 下载
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 外部行改为 批次型号 / 运营区域 / 车牌多选;按部门、按客户移入详情面板
- 车牌多选支持从 Excel 粘贴(换行/逗号/空格分隔),未匹配项显示警告
- 新增运营区域筛选:基于 136 批次区域映射(华东/华南/西南/西北)
- 新增 xlsx 数据下载,导出当前筛选结果(带表头样式与列宽)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-29 15:19:00 +08:00
parent d1acdafa7e
commit 3809e785c1
11 changed files with 670 additions and 72 deletions

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: [] },
filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [] },
total: 0,
page: 1,
totalPages: 1,
@@ -19,7 +19,7 @@ const EMPTY_RESPONSE: MonitoringResponse = {
function applyFilters(vehicles: CachedVehicle[], params: {
search: string; dept: string; customer: string; project: string;
entity: string; rentStatus: string; plate: string; platePrefix: string;
targetName: string; mileageMin: string; mileageMax: string;
targetName: string; region: string; mileageMin: string; mileageMax: string;
}): CachedVehicle[] {
let result = vehicles;
@@ -36,8 +36,12 @@ function applyFilters(vehicles: CachedVehicle[], params: {
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) result = result.filter(v => v.plate === params.plate);
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);
@@ -66,6 +70,7 @@ app.get('/', async (c) => {
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') || '',
};