feat(mileage): 点击车辆卡片展示近 15 日行驶里程明细
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:
kkfluous
2026-04-29 16:16:54 +08:00
parent f9c6155ea7
commit 7ca8ef24dc
5 changed files with 283 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ import { fetchMonitoring } from './api';
import Blur from '../../components/Blur';
import PlateMultiSelect from './PlateMultiSelect';
import { exportMileageXlsx } from './xlsx-export';
import VehicleDetailModal from './VehicleDetailModal';
const SearchableSelect = ({
options,
@@ -115,6 +116,7 @@ export default function MonitoringView() {
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' });
const [exporting, setExporting] = useState(false);
const [detailVehicle, setDetailVehicle] = useState<MonitoringVehicle | null>(null);
const [filterDate, setFilterDate] = useState(() => {
const now = new Date();
if (now.getHours() < 5) now.setDate(now.getDate() - 1);
@@ -899,10 +901,8 @@ export default function MonitoringView() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
key={v.plate}
className="bg-white px-3 py-2 rounded-xl border border-slate-50 shadow-sm flex items-center justify-between active:bg-slate-50 transition-all"
onClick={() => {
navigator.clipboard.writeText(v.plate);
}}
className="bg-white px-3 py-2 rounded-xl border border-slate-50 shadow-sm flex items-center justify-between active:bg-slate-50 cursor-pointer transition-all"
onClick={() => setDetailVehicle(v)}
>
<div className="flex items-center gap-3 overflow-hidden flex-1">
<div className="relative flex-shrink-0">
@@ -966,6 +966,8 @@ export default function MonitoringView() {
<div ref={sentinelRef} className="h-1" />
</div>
<VehicleDetailModal vehicle={detailVehicle} onClose={() => setDetailVehicle(null)} />
{/* 回到顶部按钮 */}
<AnimatePresence>
{showBackToTop && (