158 lines
7.8 KiB
TypeScript
158 lines
7.8 KiB
TypeScript
import type { RefObject } from 'react';
|
|
import { Truck } from 'lucide-react';
|
|
import { motion } from 'motion/react';
|
|
import Blur from '../../../../components/Blur';
|
|
import type { MonitoringVehicle } from '../../types';
|
|
import { vehicleStatisticTime } from '../oneos-time';
|
|
import { vehicleSourceDisplay } from '../source-display';
|
|
|
|
export default function VehicleList({
|
|
pageLoading,
|
|
vehicles,
|
|
isHighMileageAlert,
|
|
relativeNow,
|
|
isRangeMode,
|
|
onVehicleClick,
|
|
loadingMore,
|
|
hasMore,
|
|
total,
|
|
sentinelRef,
|
|
}: {
|
|
pageLoading: boolean;
|
|
vehicles: MonitoringVehicle[];
|
|
isHighMileageAlert: (vehicle: MonitoringVehicle) => boolean;
|
|
relativeNow: number;
|
|
isRangeMode: boolean;
|
|
onVehicleClick: (vehicle: MonitoringVehicle) => void;
|
|
loadingMore: boolean;
|
|
hasMore: boolean;
|
|
total: number;
|
|
sentinelRef: RefObject<HTMLDivElement | null>;
|
|
}) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
{pageLoading && (
|
|
<div className="space-y-1.5">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="bg-white px-3 py-3 rounded-xl border border-slate-50 shadow-sm flex items-center justify-between animate-pulse">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<div className="w-8 h-8 rounded-lg bg-slate-100"></div>
|
|
<div className="space-y-1.5 flex-1">
|
|
<div className="h-3 bg-slate-100 rounded w-24"></div>
|
|
<div className="h-2 bg-slate-50 rounded w-36"></div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5 text-right">
|
|
<div className="h-4 bg-slate-100 rounded w-16 ml-auto"></div>
|
|
<div className="h-2 bg-slate-50 rounded w-20 ml-auto"></div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 gap-1.5">
|
|
{vehicles.map((v) => {
|
|
const highMileageAlert = isHighMileageAlert(v);
|
|
const statisticTime = vehicleStatisticTime(v, relativeNow);
|
|
const sourceDisplay = vehicleSourceDisplay(v);
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
key={v.plate}
|
|
className="grid cursor-pointer grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5 shadow-sm transition-all hover:border-blue-200 hover:shadow-md active:bg-slate-50 md:grid-cols-[minmax(190px,1.1fr)_minmax(160px,1fr)_130px_130px] md:px-4"
|
|
onClick={() => onVehicleClick(v)}
|
|
>
|
|
<div className="flex min-w-0 items-center gap-3 overflow-hidden">
|
|
<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 ${v.isOnline ? 'bg-green-500' : 'bg-slate-300'}`} title={v.isOnline ? '在线' : '离线'}></div>
|
|
</div>
|
|
<div className="overflow-hidden flex-1">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<span className="text-xs font-black text-slate-900 font-mono"><Blur>{v.plate}</Blur></span>
|
|
<span className={`text-[8px] px-1 rounded ${v.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
|
|
{v.isOnline ? '在线' : '离线'}
|
|
</span>
|
|
<span
|
|
className={`inline-flex shrink-0 rounded px-1.5 py-0.5 text-[7px] font-black ${sourceDisplay.className}`}
|
|
title={sourceDisplay.title}
|
|
>
|
|
{sourceDisplay.label}
|
|
</span>
|
|
</div>
|
|
<div className={`text-[8px] font-bold ${statisticTime.color}`} title={statisticTime.title}>
|
|
{statisticTime.label}
|
|
</div>
|
|
<div className="flex items-center gap-1.5 md:hidden">
|
|
<span className="text-[8px] text-slate-300 font-bold">{v.rentStatus || ''}{v.department ? ` · ${v.department.replace('业务', '')}` : ''}</span>
|
|
<span className="text-[9px] font-bold text-slate-600 truncate"><Blur>{v.customer || '-'}</Blur></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="hidden min-w-0 md:block">
|
|
<div className="truncate text-xs font-bold text-slate-700"><Blur>{v.customer || '-'}</Blur></div>
|
|
<div className="mt-1 truncate text-[9px] font-medium text-slate-400">
|
|
{[v.rentStatus, v.department?.replace('业务', ''), v.project].filter(Boolean).join(' · ') || '暂无归属信息'}
|
|
</div>
|
|
</div>
|
|
<div className="hidden text-right md:block">
|
|
<div className={`text-sm font-black tabular-nums ${highMileageAlert ? 'text-red-600' : 'text-blue-600'}`}>
|
|
{Math.max(0, v.dailyKm || 0).toLocaleString()}
|
|
<span className="ml-1 text-[9px] font-bold text-slate-400">km</span>
|
|
</div>
|
|
</div>
|
|
<div className="hidden text-right md:block">
|
|
<div className="text-xs font-black tabular-nums text-slate-700">
|
|
{v.totalKm != null ? v.totalKm.toLocaleString() : '-'}
|
|
<span className="ml-1 text-[9px] font-bold text-slate-400">km</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-shrink-0 flex-col items-end text-right md:hidden">
|
|
<div className="flex items-center gap-1 mb-0.5">
|
|
{!v.isDataSynced && v.totalKm == null && (
|
|
<div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title="未对接车机数据"></div>
|
|
)}
|
|
<span className="text-[7px] font-black text-blue-600/50 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none" title={isRangeMode ? '区间内里程' : '当日里程'}>区</span>
|
|
<div className={`text-sm font-black leading-none ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-600' : 'text-blue-600') : 'text-amber-600'}`}>
|
|
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400' : 'text-slate-400'}`}>km</span></> : <span className="text-[7px] text-amber-500/70">未对接</span>}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-[7px] font-black text-slate-400/60 bg-slate-100 w-3 h-3 rounded flex items-center justify-center leading-none">总</span>
|
|
<span className="text-[8px] font-bold text-slate-300">
|
|
{v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '未对接'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{vehicles.length === 0 && !loadingMore && (
|
|
<div className="py-10 text-center bg-white rounded-2xl border border-dashed border-slate-100">
|
|
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 加载更多提示 */}
|
|
{loadingMore && (
|
|
<div className="py-4 text-center">
|
|
<span className="text-[10px] font-bold text-slate-400">加载中...</span>
|
|
</div>
|
|
)}
|
|
{!hasMore && vehicles.length > 0 && (
|
|
<div className="py-4 text-center">
|
|
<span className="text-[10px] font-bold text-slate-300">已加载全部 {total} 条</span>
|
|
</div>
|
|
)}
|
|
{/* 哨兵元素:进入视口时触发加载更多 */}
|
|
<div ref={sentinelRef} className="h-1" />
|
|
</div>
|
|
);
|
|
}
|