130 lines
7.7 KiB
TypeScript
130 lines
7.7 KiB
TypeScript
import { AnimatePresence, motion } from 'motion/react';
|
|
import { Minimize2, RotateCcw } from 'lucide-react';
|
|
import type { TargetSummary } from '../types';
|
|
import { deriveTargetTableRow, fmtKm, summarizeTargets } from './model';
|
|
|
|
interface FullscreenTargetTableProps {
|
|
isOpen: boolean;
|
|
targets: TargetSummary[];
|
|
assessmentYearMap: Record<number, number>;
|
|
onRefresh: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function FullscreenTargetTable({
|
|
isOpen,
|
|
targets,
|
|
assessmentYearMap,
|
|
onRefresh,
|
|
onClose,
|
|
}: FullscreenTargetTableProps) {
|
|
const totals = summarizeTargets(targets, assessmentYearMap);
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-[100] bg-slate-950 flex flex-col overflow-hidden"
|
|
>
|
|
<div className="flex-shrink-0 px-3 py-2 border-b border-slate-800/60 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-0.5 h-4 bg-blue-500 rounded-full"></div>
|
|
<h2 className="text-white font-bold text-xs">车型考核汇总</h2>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-[10px]">
|
|
<span className="text-slate-500">今日 <span className="text-white font-black">{fmtKm(totals.today)}</span> <span className="text-blue-400">km</span></span>
|
|
<span className="text-slate-700">|</span>
|
|
<span className="text-slate-500">累计 <span className="text-white font-black">{fmtKm(totals.cumulative)}</span> <span className="text-blue-400">km</span></span>
|
|
<span className="text-slate-700">|</span>
|
|
<span className="text-slate-500">车辆 <span className="text-white font-black">{totals.vehicleCount}</span> 台</span>
|
|
<span className="text-slate-700">|</span>
|
|
<span className="text-slate-500">所选年度完成率 <span className="text-white font-black">{totals.averageCompletion.toFixed(1)}</span> <span className="text-blue-400">%</span></span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onRefresh}
|
|
className="p-1.5 text-slate-500 hover:text-blue-400 transition-colors"
|
|
>
|
|
<RotateCcw size={13} />
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1.5 text-slate-500 hover:text-white transition-colors"
|
|
>
|
|
<Minimize2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead className="sticky top-0 bg-slate-900 z-10">
|
|
<tr className="border-b border-slate-800/60">
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase sticky left-0 bg-slate-900 z-20 min-w-[100px]">车型</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-center w-12">台数</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase min-w-[140px]">年度进度</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-right">年度任务/辆</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-emerald-500 uppercase text-center">年度达标</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-blue-400 uppercase text-center">50%达标</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-white uppercase text-right">今日里程</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-right">年度目标</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-right">已完成</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-rose-400 uppercase text-right">未完成</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-center w-14">余天</th>
|
|
<th className="px-3 py-2 text-[10px] font-bold text-blue-400 uppercase text-right">日均需完成</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-800/30">
|
|
{targets.map((target, index) => {
|
|
const row = deriveTargetTableRow(target, assessmentYearMap[target.id]);
|
|
return (
|
|
<tr key={index} className="hover:bg-slate-800/20 transition-colors">
|
|
<td className="px-3 py-3 sticky left-0 bg-slate-950 z-10 border-r border-slate-800/40">
|
|
<div className="text-xs font-bold text-white whitespace-nowrap">{target.targetName}</div>
|
|
<div className="text-[9px] text-blue-400 font-bold mt-0.5">{row.assessment?.label || '当前年度'}</div>
|
|
<div className="text-[9px] text-slate-500 mt-0.5">{target.periods.map((period, periodIndex) => <span key={periodIndex} className="block">{period}</span>)}</div>
|
|
</td>
|
|
<td className="px-3 py-3 text-xs font-bold text-slate-300 text-center">{target.vehicleCount}</td>
|
|
<td className="px-3 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${row.completion >= 90 ? 'bg-emerald-500' : row.completion >= 50 ? 'bg-amber-500' : 'bg-amber-500/60'}`}
|
|
style={{ width: `${Math.min(row.completion, 100)}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-[10px] font-black text-white w-10 text-right">{row.completion.toFixed(1)}%</span>
|
|
</div>
|
|
<div className="flex justify-between mt-1 text-[9px] text-slate-500">
|
|
<span>{fmtKm(row.completed)}</span>
|
|
<span>/ {fmtKm(row.goal)} km</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-3 py-3 text-xs text-slate-300 text-right">{fmtKm(row.taskPerVehicle)} km</td>
|
|
<td className="px-3 py-3 text-xs font-black text-emerald-400 text-center">{row.qualified}</td>
|
|
<td className="px-3 py-3 text-xs font-black text-blue-400 text-center">{row.halfQualified}</td>
|
|
<td className="px-3 py-3 text-xs font-black text-white text-right">{fmtKm(target.todayTotal)} km</td>
|
|
<td className="px-3 py-3 text-xs text-slate-400 text-right">{fmtKm(row.goal)} km</td>
|
|
<td className="px-3 py-3 text-xs text-emerald-400/80 text-right">{fmtKm(row.completed)} km</td>
|
|
<td className="px-3 py-3 text-xs font-bold text-rose-400 text-right">{fmtKm(row.remainingMileage)} km</td>
|
|
<td className="px-3 py-3 text-xs text-slate-300 text-center">{row.days}</td>
|
|
<td className="px-3 py-3 text-xs font-bold text-blue-400 text-right">
|
|
{row.assessment && row.days === 0 ? '考核已到期' : `${fmtKm(row.daily)} km`}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|