feat(mileage): 点击车辆卡片展示近 15 日行驶里程明细
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 后端新增 GET /api/mileage/vehicle/:plate/recent,返回近 N 天 + 今日的每日里程 - 缺失日补全为 dailyKm=0 + isDataSynced=false - 前端新增 VehicleDetailModal:头部信息、合计/日均/有数据天 KPI、近 N 日柱状图、每日明细列表 - 移动端从底部弹起;缺失日柱条置灰,明细行标注「未对接」 - 卡片点击改为打开弹窗(不再复制车牌) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
src/server/routes/mileage/vehicle-recent.ts
Normal file
70
src/server/routes/mileage/vehicle-recent.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Hono } from 'hono';
|
||||
import mileagePool from '../../mileage-db.js';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
interface DayRow {
|
||||
date: string;
|
||||
daily_km: string | number | null;
|
||||
source: string | null;
|
||||
}
|
||||
|
||||
function fmt(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${dd}`;
|
||||
}
|
||||
|
||||
app.get('/:plate/recent', async (c) => {
|
||||
const plate = c.req.param('plate');
|
||||
const days = Math.min(Math.max(Number(c.req.query('days')) || 15, 1), 60);
|
||||
|
||||
if (!plate) return c.json({ days: [] }, 400);
|
||||
|
||||
try {
|
||||
const [rows] = await mileagePool.execute(
|
||||
`SELECT DATE_FORMAT(stat_date, '%Y-%m-%d') AS date, daily_km, source
|
||||
FROM v_vehicle_daily_stats
|
||||
WHERE plate = ?
|
||||
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
AND stat_date <= CURDATE()
|
||||
ORDER BY stat_date`,
|
||||
[plate, days]
|
||||
) as [DayRow[], unknown];
|
||||
|
||||
// 同一 plate 同一天可能有多个数据源,取最大 daily_km
|
||||
const map = new Map<string, { dailyKm: number; source: string }>();
|
||||
for (const r of rows) {
|
||||
const km = Number(r.daily_km) || 0;
|
||||
const src = r.source || 'NONE';
|
||||
const existing = map.get(r.date);
|
||||
if (!existing || km > existing.dailyKm) {
|
||||
map.set(r.date, { dailyKm: km, source: src });
|
||||
}
|
||||
}
|
||||
|
||||
// 补全:从 N 天前到今天(含),每天一条
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const result: { date: string; dailyKm: number; isDataSynced: boolean }[] = [];
|
||||
for (let i = days; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(today.getDate() - i);
|
||||
const key = fmt(d);
|
||||
const hit = map.get(key);
|
||||
result.push({
|
||||
date: key,
|
||||
dailyKm: hit?.dailyKm ?? 0,
|
||||
isDataSynced: !!hit && hit.source !== 'NONE',
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({ plate, days: result });
|
||||
} catch (e: unknown) {
|
||||
console.error('vehicle recent error:', e);
|
||||
return c.json({ plate, days: [] }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user