import { useEffect, useMemo, useState } from 'react'; import { motion, AnimatePresence, useDragControls } from 'motion/react'; import { X, Truck } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip, Cell, } from 'recharts'; import type { MonitoringVehicle } from './types'; import { fetchVehicleRecent, type VehicleRecentDay } from './api'; import Blur from '../../components/Blur'; interface Props { vehicle: MonitoringVehicle | null; onClose: () => void; } type RangeKey = 'last15' | 'month' | 'quarter'; const RANGE_TABS: { key: RangeKey; 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()); } function formatLabel(date: string, key: RangeKey): string { // YYYY-MM-DD → MM-DD(季度时仍展示 MM-DD) void key; return date.slice(5); } export default function VehicleDetailModal({ vehicle, onClose }: Props) { const [days, setDays] = useState([]); const [loading, setLoading] = useState(false); const [range, setRange] = useState('last15'); const dragControls = useDragControls(); // 切换车辆时重置区间为默认 useEffect(() => { if (vehicle) setRange('last15'); }, [vehicle?.plate]); // eslint-disable-line react-hooks/exhaustive-deps // 拉取数据(车辆或区间变化) useEffect(() => { if (!vehicle) return; const { start, end } = rangeFor(range); setLoading(true); setDays([]); let cancelled = false; fetchVehicleRecent(vehicle.plate, { start, end }) .then(d => { if (!cancelled) setDays(d.days); }) .catch(() => { if (!cancelled) setDays([]); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [vehicle?.plate, range]); // eslint-disable-line react-hooks/exhaustive-deps // 锁滚动 useEffect(() => { if (!vehicle) return; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, [vehicle]); // 排除"今日"列(数据未到位时易引起误读) const historyDays = useMemo(() => days.filter(d => !isToday(d.date)), [days]); const stats = useMemo(() => { const totalKm = historyDays.reduce((s, d) => s + d.dailyKm, 0); const synced = historyDays.filter(d => d.isDataSynced).length; const avg = synced > 0 ? totalKm / synced : 0; const max = Math.max(1, ...historyDays.map(d => d.dailyKm)); return { totalKm, synced, avg, max, totalDays: historyDays.length }; }, [historyDays]); // 骨架天数:根据区间预估 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]); return ( {vehicle && ( { if (info.offset.y > 100 || info.velocity.y > 600) onClose(); }} 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()} > {/* iOS 风格 drag handle —— 长按下滑可关闭 */}
dragControls.start(e)} style={{ touchAction: 'none' }} >
{/* Header */}
{vehicle.plate} {vehicle.isOnline ? '在线' : '离线'}
{vehicle.rentStatus || ''} {vehicle.department ? ` · ${vehicle.department.replace('业务', '')}` : ''} {vehicle.customer ? ` · ` : ''} {vehicle.customer && {vehicle.customer}}
{/* 时间范围切换 */}
{RANGE_TABS.map(tab => ( ))}
{/* KPI cards */}
区间合计
{loading ? : <>{Math.round(stats.totalKm).toLocaleString()}km}
日均
{loading ? : <>{Math.round(stats.avg).toLocaleString()}km}
有数据天
{loading ? : <>{stats.synced}/{stats.totalDays}}
{/* Bar chart */}
行驶里程 单位 km
{loading ? ( ) : ( formatLabel(d, range)} tick={{ fontSize: 9, fill: '#94a3b8' }} tickLine={false} axisLine={false} interval="preserveStartEnd" minTickGap={6} /> [`${Math.round(Number(v) || 0).toLocaleString()} km`, '行驶里程']} labelFormatter={(d) => `日期 ${d}`} contentStyle={{ borderRadius: 12, fontSize: 11, padding: '4px 8px' }} cursor={{ fill: 'rgba(59, 130, 246, 0.06)' }} /> {historyDays.map((d, i) => ( ))} )}
{/* 每日明细 */}
每日明细
{loading ? ( ) : ( {historyDays.slice().reverse().map((d, i) => ( {d.date}
{d.isDataSynced ? <>{Math.round(d.dailyKm).toLocaleString()} km : 未对接}
))}
)}
)} ); } function SkeletonBars({ count }: { count: number }) { return (
{Array.from({ length: count }).map((_, i) => (
))}
); } function SkeletonList({ count }: { count: number }) { return (
{Array.from({ length: count }).map((_, i) => (
))}
); }