feat(mileage): 车辆明细弹窗新增时间范围切换、骨架加载与下滑关闭
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 后端 vehicle/:plate/recent 支持 start/end 任意区间,最长 366 天 - 前端弹窗加 segmented control: 近 15 天 / 本月 / 本季度,切换重新加载 - 加载时柱状图与每日明细均显示骨架,区间合计/日均/有数据天 KPI 同步骨架 - 数据回来后柱条与每行进度条带渐入动画 - 顶部加 iOS 风格 drag handle(小白条),按住下滑超过 100px 或大速度触发关闭 - 保留点击背景与 X 按钮两种关闭方式 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,21 +16,54 @@ function fmt(d: Date): string {
|
||||
return `${y}-${m}-${dd}`;
|
||||
}
|
||||
|
||||
function parseYmd(s: string): Date | null {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
||||
if (!m) return null;
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return Number.isFinite(d.getTime()) ? d : null;
|
||||
}
|
||||
|
||||
const MAX_DAYS = 366;
|
||||
|
||||
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({ plate: '', days: [] }, 400);
|
||||
|
||||
if (!plate) return c.json({ days: [] }, 400);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
// 区间参数:优先 start/end;否则回退 days(兼容旧调用)
|
||||
const startQ = c.req.query('start');
|
||||
const endQ = c.req.query('end');
|
||||
let start: Date;
|
||||
let end: Date;
|
||||
if (startQ) {
|
||||
const ps = parseYmd(startQ);
|
||||
if (!ps) return c.json({ plate, days: [] }, 400);
|
||||
start = ps;
|
||||
end = endQ ? (parseYmd(endQ) ?? today) : today;
|
||||
} else {
|
||||
const days = Math.min(Math.max(Number(c.req.query('days')) || 15, 1), MAX_DAYS);
|
||||
end = today;
|
||||
start = new Date(today);
|
||||
start.setDate(today.getDate() - (days - 1));
|
||||
}
|
||||
if (start > end) [start, end] = [end, start];
|
||||
// 限制区间长度
|
||||
const span = Math.round((end.getTime() - start.getTime()) / 86400000) + 1;
|
||||
if (span > MAX_DAYS) {
|
||||
start = new Date(end);
|
||||
start.setDate(end.getDate() - (MAX_DAYS - 1));
|
||||
}
|
||||
|
||||
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()
|
||||
WHERE plate = ? AND stat_date >= ? AND stat_date <= ?
|
||||
ORDER BY stat_date`,
|
||||
[plate, days]
|
||||
[plate, fmt(start), fmt(end)]
|
||||
) as [DayRow[], unknown];
|
||||
|
||||
// 同一 plate 同一天可能有多个数据源,取最大 daily_km
|
||||
@@ -44,23 +77,21 @@ app.get('/:plate/recent', async (c) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 补全:从 N 天前到今天(含),每天一条
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
// 补全:从 start 到 end 每天一条
|
||||
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 cursor = new Date(start);
|
||||
while (cursor <= end) {
|
||||
const key = fmt(cursor);
|
||||
const hit = map.get(key);
|
||||
result.push({
|
||||
date: key,
|
||||
dailyKm: hit?.dailyKm ?? 0,
|
||||
isDataSynced: !!hit && hit.source !== 'NONE',
|
||||
});
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
return c.json({ plate, days: result });
|
||||
return c.json({ plate, start: fmt(start), end: fmt(end), days: result });
|
||||
} catch (e: unknown) {
|
||||
console.error('vehicle recent error:', e);
|
||||
return c.json({ plate, days: [] }, 500);
|
||||
|
||||
Reference in New Issue
Block a user