fix: preserve vehicle drill date context
This commit is contained in:
@@ -160,6 +160,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
||||
|
||||
- 已完成里程考核归因、车辆详情和每日汇报跳转。
|
||||
- 里程考核年度已纳入 `mileageYear` URL 状态;目标、部门/客户归因和车辆清单下钻继承同一年度,无效年度会按目标真实年度归一。
|
||||
- 车辆级里程详情继承 `mileageStart`、`mileageEnd` 和数据源优先级;显式下钻日期严格展示所选日,预设区间继续排除尚未完整的当天数据,请求故障显示为不可用并支持重试。
|
||||
- 已完成氢能站点、客户下钻代码。
|
||||
- 已完成氢能每日趋势到加氢订单的 URL 下钻、分页和结构化故障状态;待数据源恢复后验收真实明细。
|
||||
- 已为电能总览与氢能每日趋势柱图统一 Enter/Space 键盘下钻;氢能有效日期柱提供可读名称、焦点和选中反馈。
|
||||
|
||||
@@ -1663,9 +1663,14 @@ export default function MonitoringView() {
|
||||
</div>
|
||||
|
||||
<VehicleDetailModal
|
||||
key={detailVehicle
|
||||
? `${detailVehicle.plate}|${drillContext.startDate || effectiveRange.start}|${drillContext.endDate || effectiveRange.end}`
|
||||
: 'closed'}
|
||||
vehicle={detailVehicle}
|
||||
onClose={closeVehicleDetail}
|
||||
sourcePriority={sourcePriority}
|
||||
initialStartDate={drillContext.startDate || effectiveRange.start}
|
||||
initialEndDate={drillContext.endDate || effectiveRange.end}
|
||||
/>
|
||||
|
||||
{/* 回到顶部按钮 */}
|
||||
|
||||
@@ -1,57 +1,42 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { motion, AnimatePresence, useDragControls } from 'motion/react';
|
||||
import { X, Truck } from 'lucide-react';
|
||||
import { RefreshCw, X, Truck } from 'lucide-react';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip, Cell,
|
||||
} from 'recharts';
|
||||
import type { MileageSourceGroup, MonitoringVehicle } from './types';
|
||||
import { fetchVehicleRecent, type VehicleRecentDay } from './api';
|
||||
import Blur from '../../components/Blur';
|
||||
import { EmptyState, ErrorState } from '../../components/ui/surface';
|
||||
import {
|
||||
includeCurrentDayInVehicleDetail,
|
||||
normalizeVehicleDetailContext,
|
||||
resolveVehicleDetailRange,
|
||||
vehicleDetailContextLabel,
|
||||
type VehicleDetailRangeKey,
|
||||
} from './vehicle-detail-range';
|
||||
|
||||
interface Props {
|
||||
vehicle: MonitoringVehicle | null;
|
||||
onClose: () => void;
|
||||
sourcePriority: MileageSourceGroup[];
|
||||
initialStartDate?: string;
|
||||
initialEndDate?: string;
|
||||
}
|
||||
|
||||
type RangeKey = 'last15' | 'month' | 'quarter';
|
||||
|
||||
const RANGE_TABS: { key: RangeKey; label: string }[] = [
|
||||
const RANGE_TABS: { key: VehicleDetailRangeKey; label: string }[] = [
|
||||
{ key: 'last15', label: '近 15 天' },
|
||||
{ key: 'month', label: '本月' },
|
||||
{ key: 'quarter', label: '本季度' },
|
||||
];
|
||||
|
||||
function fmtYmd(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}`;
|
||||
}
|
||||
|
||||
function rangeFor(key: RangeKey): { start: string; end: string; rangeLabel: string } {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const end = fmtYmd(today);
|
||||
if (key === 'last15') {
|
||||
const start = new Date(today);
|
||||
start.setDate(today.getDate() - 14);
|
||||
return { start: fmtYmd(start), end, rangeLabel: '近 15 天' };
|
||||
}
|
||||
if (key === 'month') {
|
||||
const start = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return { start: fmtYmd(start), end, rangeLabel: '本月' };
|
||||
}
|
||||
const q = Math.floor(today.getMonth() / 3);
|
||||
const start = new Date(today.getFullYear(), q * 3, 1);
|
||||
return { start: fmtYmd(start), end, rangeLabel: '本季度' };
|
||||
}
|
||||
|
||||
function isToday(date: string): boolean {
|
||||
return date === fmtYmd(new Date());
|
||||
const now = new Date();
|
||||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
return date === today;
|
||||
}
|
||||
|
||||
function formatLabel(date: string, key: RangeKey): string {
|
||||
function formatLabel(date: string, key: VehicleDetailRangeKey): string {
|
||||
// YYYY-MM-DD → MM-DD(季度时仍展示 MM-DD)
|
||||
void key;
|
||||
return date.slice(5);
|
||||
@@ -63,30 +48,65 @@ function daySourceLabel(day: VehicleRecentDay): string {
|
||||
return day.isDataSynced ? '来源待接口' : '无数据';
|
||||
}
|
||||
|
||||
export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }: Props) {
|
||||
export default function VehicleDetailModal({
|
||||
vehicle,
|
||||
onClose,
|
||||
sourcePriority,
|
||||
initialStartDate,
|
||||
initialEndDate,
|
||||
}: Props) {
|
||||
const contextRange = useMemo(
|
||||
() => normalizeVehicleDetailContext(initialStartDate, initialEndDate),
|
||||
[initialEndDate, initialStartDate],
|
||||
);
|
||||
const [days, setDays] = useState<VehicleRecentDay[]>([]);
|
||||
const [loadedRequestKey, setLoadedRequestKey] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [range, setRange] = useState<RangeKey>('last15');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [requestVersion, setRequestVersion] = useState(0);
|
||||
const [range, setRange] = useState<VehicleDetailRangeKey>(() => (
|
||||
contextRange ? 'context' : 'last15'
|
||||
));
|
||||
const dragControls = useDragControls();
|
||||
|
||||
// 切换车辆时重置区间为默认
|
||||
useEffect(() => {
|
||||
if (vehicle) setRange('last15');
|
||||
}, [vehicle?.plate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const resolvedRange = useMemo(
|
||||
() => resolveVehicleDetailRange(range, contextRange),
|
||||
[contextRange, range],
|
||||
);
|
||||
const sourceKey = sourcePriority.join(',');
|
||||
const requestKey = vehicle
|
||||
? `${vehicle.plate}|${resolvedRange.start}|${resolvedRange.end}|${sourceKey}`
|
||||
: null;
|
||||
const hasSnapshot = requestKey !== null && loadedRequestKey === requestKey;
|
||||
const rangeTabs = useMemo(() => (
|
||||
contextRange
|
||||
? [{ key: 'context' as const, label: vehicleDetailContextLabel(contextRange) }, ...RANGE_TABS]
|
||||
: RANGE_TABS
|
||||
), [contextRange]);
|
||||
|
||||
// 拉取数据(车辆或区间变化)
|
||||
useEffect(() => {
|
||||
if (!vehicle) return;
|
||||
const { start, end } = rangeFor(range);
|
||||
if (!vehicle || !requestKey) return;
|
||||
setLoading(true);
|
||||
setDays([]);
|
||||
setError(null);
|
||||
let cancelled = false;
|
||||
fetchVehicleRecent(vehicle.plate, { start, end, sourcePriority })
|
||||
.then(d => { if (!cancelled) setDays(d.days); })
|
||||
.catch(() => { if (!cancelled) setDays([]); })
|
||||
fetchVehicleRecent(vehicle.plate, {
|
||||
start: resolvedRange.start,
|
||||
end: resolvedRange.end,
|
||||
sourcePriority,
|
||||
})
|
||||
.then(d => {
|
||||
if (cancelled) return;
|
||||
setDays(d.days);
|
||||
setLoadedRequestKey(requestKey);
|
||||
})
|
||||
.catch(cause => {
|
||||
if (!cancelled) {
|
||||
setError(cause instanceof Error ? cause.message : '车辆近期里程加载失败,请稍后重试。');
|
||||
}
|
||||
})
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [vehicle?.plate, range, sourcePriority]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [requestKey, requestVersion]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 锁滚动
|
||||
useEffect(() => {
|
||||
@@ -95,8 +115,11 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}, [vehicle]);
|
||||
|
||||
// 排除"今日"列(数据未到位时易引起误读)
|
||||
const historyDays = useMemo(() => days.filter(d => !isToday(d.date)), [days]);
|
||||
// 预设区间排除未完整的今日;显式下钻区间严格保留用户选择的日期。
|
||||
const historyDays = useMemo(() => {
|
||||
if (!hasSnapshot) return [];
|
||||
return includeCurrentDayInVehicleDetail(range) ? days : days.filter(d => !isToday(d.date));
|
||||
}, [days, hasSnapshot, range]);
|
||||
const stats = useMemo(() => {
|
||||
const totalKm = historyDays.reduce((s, d) => s + d.dailyKm, 0);
|
||||
const synced = historyDays.filter(d => d.isDataSynced).length;
|
||||
@@ -107,12 +130,12 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
|
||||
// 骨架天数:根据区间预估
|
||||
const skeletonCount = useMemo(() => {
|
||||
if (range === 'last15') return 15;
|
||||
const { start, end } = rangeFor(range);
|
||||
const s = new Date(start);
|
||||
const e = new Date(end);
|
||||
return Math.max(1, Math.round((e.getTime() - s.getTime()) / 86400000));
|
||||
}, [range]);
|
||||
const start = new Date(`${resolvedRange.start}T00:00:00`);
|
||||
const end = new Date(`${resolvedRange.end}T00:00:00`);
|
||||
return Math.max(1, Math.round((end.getTime() - start.getTime()) / 86400000) + 1);
|
||||
}, [resolvedRange.end, resolvedRange.start]);
|
||||
const showInitialLoading = loading && !hasSnapshot;
|
||||
const showUnavailable = !!error && !hasSnapshot;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -142,6 +165,9 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
}}
|
||||
className="bg-white w-full md:max-w-md md:rounded-3xl rounded-t-3xl shadow-2xl max-h-[92vh] overflow-hidden flex flex-col touch-pan-y"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="vehicle-detail-title"
|
||||
>
|
||||
{/* iOS 风格 drag handle —— 长按下滑可关闭 */}
|
||||
<div
|
||||
@@ -160,7 +186,7 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-black text-slate-900 font-mono truncate"><Blur>{vehicle.plate}</Blur></span>
|
||||
<span id="vehicle-detail-title" className="text-sm font-black text-slate-900 font-mono truncate"><Blur>{vehicle.plate}</Blur></span>
|
||||
<span className={`text-[8px] px-1 rounded font-bold ${vehicle.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'}`}>
|
||||
{vehicle.isOnline ? '在线' : '离线'}
|
||||
</span>
|
||||
@@ -173,19 +199,22 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 -mr-1 text-slate-400 hover:text-slate-700 flex-shrink-0">
|
||||
<button type="button" onClick={onClose} aria-label="关闭车辆详情" className="p-2 -mr-1 text-slate-400 hover:text-slate-700 flex-shrink-0">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 时间范围切换 */}
|
||||
<div className="px-4 pt-3">
|
||||
<div className="relative inline-flex bg-slate-100 p-0.5 rounded-lg">
|
||||
{RANGE_TABS.map(tab => (
|
||||
<div className="relative inline-flex max-w-full overflow-x-auto bg-slate-100 p-0.5 rounded-lg" role="tablist" aria-label="车辆里程区间">
|
||||
{rangeTabs.map(tab => (
|
||||
<button
|
||||
type="button"
|
||||
key={tab.key}
|
||||
onClick={() => setRange(tab.key)}
|
||||
className={`relative px-3 py-1 text-[10px] font-bold rounded-md transition-colors ${range === tab.key ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
role="tab"
|
||||
aria-selected={range === tab.key}
|
||||
className={`relative shrink-0 px-3 py-1 text-[10px] font-bold rounded-md transition-colors ${range === tab.key ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
{range === tab.key && (
|
||||
<motion.div
|
||||
@@ -198,28 +227,48 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{loading && hasSnapshot ? (
|
||||
<span className="ml-2 inline-flex items-center gap-1 text-[9px] font-bold text-slate-400">
|
||||
<RefreshCw size={10} className="animate-spin" /> 刷新中
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="px-4 pt-3">
|
||||
<ErrorState
|
||||
title="车辆近期里程加载失败"
|
||||
message={hasSnapshot ? `${error} 当前保留上一次成功加载的数据。` : error}
|
||||
onRetry={() => setRequestVersion(version => version + 1)}
|
||||
retrying={loading}
|
||||
variant="inline"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* KPI cards */}
|
||||
<div className="px-4 py-3 grid grid-cols-3 gap-2">
|
||||
<div className="bg-slate-50 rounded-xl p-2.5">
|
||||
<div className="text-[9px] font-bold text-slate-400 uppercase">区间合计</div>
|
||||
<div className="text-base font-black text-slate-900 leading-tight">
|
||||
{loading ? <span className="inline-block h-4 w-14 bg-slate-200 rounded animate-pulse align-middle" />
|
||||
{showInitialLoading ? <span className="inline-block h-4 w-14 bg-slate-200 rounded animate-pulse align-middle" />
|
||||
: showUnavailable ? <span className="text-slate-300">—</span>
|
||||
: <>{Math.round(stats.totalKm).toLocaleString()}<span className="text-[9px] font-bold text-slate-400 ml-0.5">km</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-2.5">
|
||||
<div className="text-[9px] font-bold text-slate-400 uppercase">日均</div>
|
||||
<div className="text-base font-black text-slate-900 leading-tight">
|
||||
{loading ? <span className="inline-block h-4 w-10 bg-slate-200 rounded animate-pulse align-middle" />
|
||||
{showInitialLoading ? <span className="inline-block h-4 w-10 bg-slate-200 rounded animate-pulse align-middle" />
|
||||
: showUnavailable ? <span className="text-slate-300">—</span>
|
||||
: <>{Math.round(stats.avg).toLocaleString()}<span className="text-[9px] font-bold text-slate-400 ml-0.5">km</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-2.5">
|
||||
<div className="text-[9px] font-bold text-slate-400 uppercase">有数据天</div>
|
||||
<div className="text-base font-black text-slate-900 leading-tight">
|
||||
{loading ? <span className="inline-block h-4 w-12 bg-slate-200 rounded animate-pulse align-middle" />
|
||||
{showInitialLoading ? <span className="inline-block h-4 w-12 bg-slate-200 rounded animate-pulse align-middle" />
|
||||
: showUnavailable ? <span className="text-slate-300">—</span>
|
||||
: <>{stats.synced}<span className="text-[9px] font-bold text-slate-400 ml-0.5">/{stats.totalDays}</span></>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,8 +282,12 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-50">
|
||||
<div className="h-[140px]">
|
||||
{loading ? (
|
||||
{showInitialLoading ? (
|
||||
<SkeletonBars count={Math.min(skeletonCount, 30)} />
|
||||
) : showUnavailable ? (
|
||||
<div className="flex h-full items-center justify-center text-[10px] font-bold text-slate-300">所选区间趋势暂不可用</div>
|
||||
) : historyDays.length === 0 ? (
|
||||
<EmptyState title="暂无里程数据" description="所选区间没有可展示的车辆里程" variant="inline" />
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={historyDays} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
|
||||
@@ -269,8 +322,12 @@ export default function VehicleDetailModal({ vehicle, onClose, sourcePriority }:
|
||||
{/* 每日明细 */}
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
<div className="text-[10px] font-bold text-slate-500 mb-1.5">每日明细</div>
|
||||
{loading ? (
|
||||
{showInitialLoading ? (
|
||||
<SkeletonList count={Math.min(skeletonCount, 15)} />
|
||||
) : showUnavailable ? (
|
||||
<div className="py-6 text-center text-[10px] font-bold text-slate-300">请重试加载车辆每日明细</div>
|
||||
) : historyDays.length === 0 ? (
|
||||
<div className="py-6 text-center text-[10px] font-bold text-slate-300">所选区间暂无每日明细</div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={range}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
includeCurrentDayInVehicleDetail,
|
||||
normalizeVehicleDetailContext,
|
||||
resolveVehicleDetailRange,
|
||||
vehicleDetailContextLabel,
|
||||
} from './vehicle-detail-range.js';
|
||||
|
||||
const NOW = new Date(2026, 7, 7, 12, 0, 0);
|
||||
|
||||
test('normalizes and preserves a selected vehicle drill range', () => {
|
||||
const context = normalizeVehicleDetailContext('2026-08-07', '2026-08-05');
|
||||
assert.deepEqual(context, { start: '2026-08-05', end: '2026-08-07' });
|
||||
assert.deepEqual(resolveVehicleDetailRange('context', context, NOW), context);
|
||||
});
|
||||
|
||||
test('rejects invalid context dates and resolves calendar presets', () => {
|
||||
assert.equal(normalizeVehicleDetailContext('2026-02-30', '2026-03-01'), null);
|
||||
assert.deepEqual(resolveVehicleDetailRange('last15', null, NOW), {
|
||||
start: '2026-07-24',
|
||||
end: '2026-08-07',
|
||||
});
|
||||
assert.deepEqual(resolveVehicleDetailRange('month', null, NOW), {
|
||||
start: '2026-08-01',
|
||||
end: '2026-08-07',
|
||||
});
|
||||
assert.deepEqual(resolveVehicleDetailRange('quarter', null, NOW), {
|
||||
start: '2026-07-01',
|
||||
end: '2026-08-07',
|
||||
});
|
||||
});
|
||||
|
||||
test('labels selected dates and includes today only for explicit context', () => {
|
||||
const context = { start: '2026-08-07', end: '2026-08-07' };
|
||||
assert.equal(vehicleDetailContextLabel(context), '8月7日');
|
||||
assert.equal(includeCurrentDayInVehicleDetail('context'), true);
|
||||
assert.equal(includeCurrentDayInVehicleDetail('last15'), false);
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
export type VehicleDetailRangeKey = 'context' | 'last15' | 'month' | 'quarter';
|
||||
|
||||
export interface VehicleDetailDateRange {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
|
||||
function fmtYmd(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function validDate(value?: string): string | null {
|
||||
if (!value) return null;
|
||||
const match = DATE_PATTERN.exec(value);
|
||||
if (!match) return null;
|
||||
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
||||
return fmtYmd(date) === value ? value : null;
|
||||
}
|
||||
|
||||
export function normalizeVehicleDetailContext(
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
): VehicleDetailDateRange | null {
|
||||
const start = validDate(startDate);
|
||||
const end = validDate(endDate);
|
||||
if (!start || !end) return null;
|
||||
return start <= end ? { start, end } : { start: end, end: start };
|
||||
}
|
||||
|
||||
export function resolveVehicleDetailRange(
|
||||
key: VehicleDetailRangeKey,
|
||||
context: VehicleDetailDateRange | null,
|
||||
now = new Date(),
|
||||
): VehicleDetailDateRange {
|
||||
if (key === 'context' && context) return context;
|
||||
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const end = fmtYmd(today);
|
||||
if (key === 'month') {
|
||||
return { start: fmtYmd(new Date(today.getFullYear(), today.getMonth(), 1)), end };
|
||||
}
|
||||
if (key === 'quarter') {
|
||||
const quarterStartMonth = Math.floor(today.getMonth() / 3) * 3;
|
||||
return { start: fmtYmd(new Date(today.getFullYear(), quarterStartMonth, 1)), end };
|
||||
}
|
||||
|
||||
const start = new Date(today);
|
||||
start.setDate(today.getDate() - 14);
|
||||
return { start: fmtYmd(start), end };
|
||||
}
|
||||
|
||||
export function vehicleDetailContextLabel(context: VehicleDetailDateRange): string {
|
||||
if (context.start === context.end) {
|
||||
return `${Number(context.start.slice(5, 7))}月${Number(context.start.slice(8, 10))}日`;
|
||||
}
|
||||
return '所选区间';
|
||||
}
|
||||
|
||||
export function includeCurrentDayInVehicleDetail(key: VehicleDetailRangeKey): boolean {
|
||||
return key === 'context';
|
||||
}
|
||||
@@ -98,7 +98,11 @@ app.get('/:plate/recent', async (c) => {
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
console.error('vehicle recent error:', e);
|
||||
return c.json({ plate, days: [] }, 500);
|
||||
return c.json({
|
||||
error: '车辆近期里程服务暂时不可用',
|
||||
code: 'MILEAGE_RECENT_UNAVAILABLE',
|
||||
retryable: true,
|
||||
}, 503);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user