152 lines
7.1 KiB
TypeScript
152 lines
7.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import { ArrowUpDown, Calendar, Search, Truck, X } from 'lucide-react';
|
|
import Blur from '../../../components/Blur';
|
|
import type { TargetVehicle } from '../types';
|
|
import {
|
|
filterAndSortTargetVehicles,
|
|
fmtKm,
|
|
totalTodayMileage,
|
|
type VehicleSort,
|
|
} from './model';
|
|
|
|
interface AllVehiclesPanelProps {
|
|
targetId: number | null;
|
|
targetName: string;
|
|
vehicles: TargetVehicle[];
|
|
date: string;
|
|
loading: boolean;
|
|
onDateChange: (date: string) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function AllVehiclesPanel({
|
|
targetId,
|
|
targetName,
|
|
vehicles,
|
|
date,
|
|
loading,
|
|
onDateChange,
|
|
onClose,
|
|
}: AllVehiclesPanelProps) {
|
|
const [search, setSearch] = useState('');
|
|
const [sort, setSort] = useState<VehicleSort>('desc');
|
|
const filteredVehicles = filterAndSortTargetVehicles(vehicles, search, sort);
|
|
const totalKm = totalTodayMileage(filteredVehicles);
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{targetId !== null && (
|
|
<>
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
className="fixed inset-0 z-[110] bg-slate-950/60 backdrop-blur-sm"
|
|
/>
|
|
<motion.div
|
|
initial={{ x: '100%' }}
|
|
animate={{ x: 0 }}
|
|
exit={{ x: '100%' }}
|
|
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
className="fixed top-0 right-0 bottom-0 w-full max-w-sm z-[120] bg-white shadow-2xl flex flex-col"
|
|
>
|
|
<div className="p-6 border-b border-slate-100 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-black text-slate-900">{targetName}</h3>
|
|
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">车辆实时明细清单</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
onClose();
|
|
setSearch('');
|
|
}}
|
|
className="p-2 hover:bg-slate-100 rounded-full transition-colors text-slate-400 hover:text-slate-900"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="px-6 py-3 border-b border-slate-50 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
|
|
<input
|
|
type="text"
|
|
placeholder="搜索车牌号..."
|
|
value={search}
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
className="w-full pl-9 pr-3 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
|
/>
|
|
</div>
|
|
<div className="relative flex-shrink-0">
|
|
<Calendar className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" size={13} />
|
|
<input
|
|
type="date"
|
|
value={date}
|
|
onChange={(event) => onDateChange(event.target.value)}
|
|
className="pl-8 pr-2 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all w-[130px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
|
{loading ? '加载中...' : `${filteredVehicles.length} 辆 · 合计 ${fmtKm(totalKm)} km`}
|
|
</span>
|
|
<button
|
|
onClick={() => setSort(previous => previous === 'desc' ? 'asc' : 'desc')}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 text-blue-600 rounded-lg text-[10px] font-black active:scale-95 transition-all"
|
|
>
|
|
<ArrowUpDown size={12} />
|
|
{sort === 'desc' ? '降序' : '升序'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-2 no-scrollbar">
|
|
{filteredVehicles.map(vehicle => (
|
|
<div key={vehicle.plateNumber} className="bg-white px-3 py-2 rounded-xl border border-slate-50 shadow-sm flex items-center justify-between hover:border-blue-200 transition-all">
|
|
<div className="flex items-center gap-3 overflow-hidden flex-1">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center">
|
|
<Truck size={14} className="text-slate-400" />
|
|
</div>
|
|
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${vehicle.isOnline ? 'bg-green-500' : 'bg-slate-300'}`} />
|
|
</div>
|
|
<div className="overflow-hidden flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-xs font-black text-slate-900 font-mono"><Blur>{vehicle.plateNumber}</Blur></span>
|
|
<span className={`text-[8px] px-1 rounded ${vehicle.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
|
|
{vehicle.isOnline ? '在线' : '离线'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-[8px] text-slate-300 font-bold">{vehicle.rentStatus || ''}{vehicle.department ? ` · ${vehicle.department.replace('业务', '')}` : ''}</span>
|
|
<span className="text-[9px] font-bold text-slate-600 truncate">{vehicle.customer || '-'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right flex-shrink-0 ml-2">
|
|
<div className="text-sm font-black text-blue-600">{vehicle.todayMileage.toLocaleString()} <span className="text-[8px] text-slate-400">KM</span></div>
|
|
<div className="text-[9px] font-bold text-slate-300 mt-0.5">累计: {fmtKm(vehicle.totalMileage || 0)} km</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-4 bg-slate-50 border-t border-slate-100">
|
|
<button
|
|
onClick={onClose}
|
|
className="w-full py-3 bg-slate-900 text-white rounded-xl text-sm font-bold shadow-lg shadow-slate-200 active:scale-[0.98] transition-all"
|
|
>
|
|
关闭明细
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|