feat: 车牌区域筛选、型号批次筛选、回到顶部修复、删除涨跌幅
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 新增车牌区域筛选(粤/沪/浙+数量),替代旧地区代码
- 新增型号批次筛选(从考核目标名称筛选车辆)
- 客户/部门增加"无值"选项筛选空值
- 修复回到顶部按钮在iOS上失效
- 删除KPI卡片涨跌幅百分比显示
- 全屏刷新按钮实际触发数据重新加载+加载动画
- 统计报表全屏刷新按钮修复

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-02 11:21:38 +08:00
parent adc9c3a9db
commit 8822ddf8ae
4 changed files with 76 additions and 23 deletions

View File

@@ -45,7 +45,8 @@ interface CachedVehicle {
interface MonitoringCache {
vehicles: CachedVehicle[];
stats: { totalToday: number; totalAll: number; vehicleCount: number };
filters: { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[]; rentStatuses: string[] };
filters: { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[]; rentStatuses: string[]; platePrefixes: { prefix: string; count: number }[]; targetNames: string[] };
targetPlatesMap: Map<string, Set<string>>;
updatedAt: string;
}
@@ -57,7 +58,7 @@ async function refreshMonitoringCache() {
const start = Date.now();
// 并行查询两个数据库
const [mileageResult, yesterdayResult, infoRows] = await Promise.all([
const [mileageResult, yesterdayResult, infoRows, targetRows] = await Promise.all([
(async () => {
const [dateRows] = await mileagePool.execute(
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
@@ -85,6 +86,9 @@ async function refreshMonitoringCache() {
return map;
})(),
pool.execute(VEHICLE_INFO_SQL).then(([rows]) => rows as any[]),
pool.execute(`SELECT t.id, t.target_name, v.plate_number FROM tab_mileage_assessment_target t
JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0
WHERE t.is_deleted = 0`).then(([rows]) => rows as any[]),
]);
// 车辆关联信息 map
@@ -93,6 +97,17 @@ async function refreshMonitoringCache() {
infoMap.set(row.plate, row);
}
// 型号批次→车牌映射
const targetNameMap = new Map<number, string>();
const targetPlatesMap = new Map<string, Set<string>>(); // targetName -> plates
for (const r of targetRows) {
targetNameMap.set(r.id, r.target_name);
const set = targetPlatesMap.get(r.target_name) || new Set();
set.add(r.plate_number);
targetPlatesMap.set(r.target_name, set);
}
const targetNames = Array.from(targetPlatesMap.keys());
// 去重:同一 plate 取 daily_km 最大的
const mileageMap = new Map<string, any>();
for (const row of mileageResult) {
@@ -143,11 +158,18 @@ async function refreshMonitoringCache() {
const projects = Array.from(new Set(vehicles.map(v => v.project).filter(Boolean))) as string[];
const entities = Array.from(new Set(vehicles.map(v => v.entity).filter(Boolean))) as string[];
const rentStatuses = Array.from(new Set(vehicles.map(v => v.rentStatus).filter(Boolean))) as string[];
const prefixCount = new Map<string, number>();
for (const v of vehicles) {
const p = v.plate.charAt(0);
prefixCount.set(p, (prefixCount.get(p) || 0) + 1);
}
const platePrefixes = Array.from(prefixCount.entries()).map(([prefix, count]) => ({ prefix, count })).sort((a, b) => b.count - a.count);
monitoringCache = {
vehicles,
stats: { totalToday, totalAll, vehicleCount: vehicles.length },
filters: { departments, customers, plates, projects, entities, rentStatuses },
filters: { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames },
targetPlatesMap,
updatedAt: new Date().toISOString(),
};
@@ -200,7 +222,7 @@ async function queryDateMileage(dateStr: string): Promise<{ vehicles: CachedVehi
// GET /monitoring — 从缓存取数据(或指定日期实时查询),支持筛选/排序/分页
app.get('/monitoring', async (c) => {
const emptyResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() };
const emptyResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() };
const sortBy = c.req.query('sortBy') || 'today';
const sortOrder = c.req.query('sortOrder') || 'desc';
@@ -213,6 +235,8 @@ app.get('/monitoring', async (c) => {
const entity = c.req.query('entity') || '';
const mileageMin = c.req.query('mileageMin') || '';
const mileageMax = c.req.query('mileageMax') || '';
const platePrefix = c.req.query('platePrefix') || '';
const targetName = c.req.query('targetName') || '';
const plate = c.req.query('plate') || '';
const rentStatus = c.req.query('rentStatus') || '';
const date = c.req.query('date') || '';
@@ -236,6 +260,8 @@ app.get('/monitoring', async (c) => {
projects: Array.from(new Set(allVehicles.map(v => v.project).filter(Boolean))) as string[],
entities: Array.from(new Set(allVehicles.map(v => v.entity).filter(Boolean))) as string[],
rentStatuses: Array.from(new Set(allVehicles.map(v => v.rentStatus).filter(Boolean))) as string[],
platePrefixes: (() => { const m = new Map<string, number>(); for (const v of allVehicles) { const p = v.plate.charAt(0); m.set(p, (m.get(p) || 0) + 1); } return Array.from(m.entries()).map(([prefix, count]) => ({ prefix, count })).sort((a, b) => b.count - a.count); })(),
targetNames: monitoringCache?.filters.targetNames || [],
};
} catch (e) {
console.error('monitoring date query error:', e);
@@ -264,6 +290,12 @@ app.get('/monitoring', async (c) => {
if (entity) vehicles = vehicles.filter(v => v.entity === entity);
if (rentStatus) vehicles = vehicles.filter(v => v.rentStatus === rentStatus);
if (plate) vehicles = vehicles.filter(v => v.plate === plate);
if (platePrefix) vehicles = vehicles.filter(v => v.plate.startsWith(platePrefix));
if (targetName && monitoringCache?.targetPlatesMap) {
const tPlates = monitoringCache.targetPlatesMap.get(targetName);
if (tPlates) vehicles = vehicles.filter(v => tPlates.has(v.plate));
else vehicles = [];
}
if (mileageMin) vehicles = vehicles.filter(v => v.dailyKm >= Number(mileageMin));
if (mileageMax) vehicles = vehicles.filter(v => v.dailyKm <= Number(mileageMax));