238 lines
14 KiB
TypeScript
238 lines
14 KiB
TypeScript
import { AnimatePresence, motion } from 'motion/react';
|
|
import { ChevronDown, Maximize2, Truck } from 'lucide-react';
|
|
import Blur from '../../../components/Blur';
|
|
import type { TargetSummary, TargetVehicle } from '../types';
|
|
import { fmtDateLabel, fmtKm, fmtPercent, getTargetAssessment } from './model';
|
|
|
|
interface TargetDetailPanelProps {
|
|
target: TargetSummary | undefined;
|
|
expandedTargetId: number | null;
|
|
selectedYear: number | undefined;
|
|
vehicles: TargetVehicle[];
|
|
currentDateLabel: string;
|
|
onOpenFullscreen: () => void;
|
|
onExpandTarget: (targetId: number) => void;
|
|
onAssessmentYearChange: (targetId: number, year: number) => void;
|
|
onViewAll: (target: TargetSummary) => void;
|
|
}
|
|
|
|
export default function TargetDetailPanel({
|
|
target,
|
|
expandedTargetId,
|
|
selectedYear,
|
|
vehicles,
|
|
currentDateLabel,
|
|
onOpenFullscreen,
|
|
onExpandTarget,
|
|
onAssessmentYearChange,
|
|
onViewAll,
|
|
}: TargetDetailPanelProps) {
|
|
const assessment = target ? getTargetAssessment(target, selectedYear) : null;
|
|
const primaryCompletion = assessment?.completionRate ?? target?.avgCompletion ?? 0;
|
|
const primaryQualified = assessment?.qualifiedCount ?? target?.yearQualifiedCount ?? 0;
|
|
const primaryQualifiedLabel = assessment ? `${assessment.label}达标:` : '达标:';
|
|
|
|
return (
|
|
<div className="w-full landscape:w-1/3 flex-shrink-0 space-y-2 flex flex-col landscape:overflow-hidden">
|
|
<div className="flex items-center justify-between px-2 flex-shrink-0">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<div className="w-1 h-4 bg-blue-600 rounded-full" />
|
|
<div className="min-w-0">
|
|
<h3 className="truncate text-xs font-black text-slate-700">当前车型考核详情</h3>
|
|
<p className="mt-0.5 truncate text-[9px] font-bold text-slate-400">{target?.targetName || '请选择车型'}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onOpenFullscreen}
|
|
className="p-1.5 bg-white text-slate-400 rounded-lg border border-slate-100 shadow-sm hover:text-blue-600 transition-colors"
|
|
>
|
|
<Maximize2 size={14} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-1.5 overflow-y-auto no-scrollbar pb-2">
|
|
{target && (
|
|
<div
|
|
className="bg-white px-3 py-2 rounded-xl border border-slate-100 shadow-sm flex flex-col active:bg-slate-50 transition-all cursor-pointer"
|
|
onClick={() => onExpandTarget(target.id)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 overflow-hidden flex-1">
|
|
<div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center flex-shrink-0">
|
|
<Truck size={14} className="text-slate-400" />
|
|
</div>
|
|
<div className="overflow-hidden flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-xs font-black text-slate-900">{target.targetName}</span>
|
|
<span className="text-[8px] px-1 rounded bg-blue-50 text-blue-600 font-bold">{target.vehicleCount}台</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-[9px] text-slate-400">{assessment ? `${assessment.label}完成:` : '完成率:'}</span>
|
|
<span className={`text-[9px] font-bold ${primaryCompletion >= 90 ? 'text-emerald-500' : 'text-blue-500'}`}>{fmtPercent(primaryCompletion)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-[9px] text-slate-400">{primaryQualifiedLabel}</span>
|
|
<span className="text-[9px] font-bold text-slate-600">{primaryQualified}台</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right flex-shrink-0 ml-2 flex items-center gap-3">
|
|
<div className="flex flex-col items-end">
|
|
<div className="text-sm font-black text-slate-900 leading-none mb-0.5">
|
|
{fmtKm(target.todayTotal)} <span className="text-[8px] text-slate-300 font-bold uppercase">KM</span>
|
|
</div>
|
|
<div className="text-[8px] font-bold text-slate-300">
|
|
累计: {fmtKm(target.cumulativeTotal)} KM
|
|
</div>
|
|
</div>
|
|
<motion.div
|
|
animate={{ rotate: 180 }}
|
|
className="text-slate-300"
|
|
>
|
|
<ChevronDown size={14} />
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence initial={false} mode="wait">
|
|
{expandedTargetId === target.id && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.18, ease: 'easeOut' }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="pt-3 mt-2 border-t border-slate-50 grid grid-cols-2 gap-x-4 gap-y-3">
|
|
<div className="col-span-2 flex items-center justify-between gap-3 bg-blue-50/70 p-2 rounded-lg">
|
|
<span className="text-[10px] font-black text-blue-700">考核年度</span>
|
|
<select
|
|
value={assessment?.yearNumber || ''}
|
|
onClick={(event) => event.stopPropagation()}
|
|
onChange={(event) => {
|
|
event.stopPropagation();
|
|
onAssessmentYearChange(target.id, Number(event.target.value));
|
|
}}
|
|
className="bg-white border border-blue-100 rounded-lg px-2 py-1 text-[10px] font-bold text-blue-700 outline-none"
|
|
>
|
|
{target.yearlyAssessments.map(item => (
|
|
<option key={item.yearNumber} value={item.yearNumber}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="col-span-2 bg-slate-50/80 rounded-lg p-2 grid grid-cols-2 gap-3">
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">总考核区间</p>
|
|
{target.periods.map((period, index) => (
|
|
<p key={index} className="text-[10px] font-black text-slate-700">{period}</p>
|
|
))}
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">总考核里程</p>
|
|
<p className="text-[10px] font-black text-slate-700">{fmtKm(target.totalMileagePerVehicle * target.vehicleCount)} km</p>
|
|
</div>
|
|
</div>
|
|
{assessment ? (
|
|
<>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}区间</p>
|
|
{(assessment.periods.length > 0 ? assessment.periods : [`${assessment.startDate} ~ ${assessment.endDate}`]).map((period, index) => (
|
|
<p key={index} className="text-[10px] font-black text-slate-700">{period}</p>
|
|
))}
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}任务/辆</p>
|
|
<p className="text-[10px] font-black text-slate-700">{fmtKm(target.annualMileagePerVehicle * assessment.yearNumber)} km</p>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}已进入车辆</p>
|
|
<p className="text-[10px] font-black text-blue-600">{assessment.vehicleCount} 台</p>
|
|
{assessment.vehicleCount < target.vehicleCount && (
|
|
<p className="text-[8px] font-bold text-slate-400">其余 {target.vehicleCount - assessment.vehicleCount} 台尚未进入{assessment.label}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}需完成</p>
|
|
<p className="text-[10px] font-black text-slate-700">{fmtKm(assessment.target)} km</p>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}已完成</p>
|
|
<p className="text-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
|
|
<p className="text-[8px] font-bold text-slate-300">
|
|
数据截至 {assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}
|
|
</p>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}完成率</p>
|
|
<p className="text-[10px] font-black text-blue-600">{fmtPercent(assessment.completionRate)}</p>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}达标率</p>
|
|
<p className="text-[10px] font-black text-emerald-600">{fmtPercent(assessment.qualifiedRate)} ({assessment.qualifiedCount}台)</p>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}剩余里程</p>
|
|
<p className="text-[10px] font-black text-rose-500">{fmtKm(assessment.remaining)} km</p>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}日均需完成</p>
|
|
<p className="text-[10px] font-black text-blue-500">
|
|
{assessment.daysLeft > 0 ? `${fmtKm(assessment.dailyTarget)} km` : '考核已到期'}
|
|
</p>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="col-span-2 bg-slate-50 p-2 rounded-lg text-[10px] font-bold text-slate-400">
|
|
暂无第一年度车辆,当前车型车辆已进入后续考核年度。
|
|
</div>
|
|
)}
|
|
<div className="col-span-2 flex items-center justify-between bg-slate-50 p-2 rounded-lg">
|
|
<span className="text-[9px] font-bold text-slate-500">{assessment ? `${assessment.label}剩余考核天数` : '剩余考核天数'}</span>
|
|
<span className="text-[10px] font-black text-slate-900">{assessment?.daysLeft ?? target.daysLeft} 天</span>
|
|
</div>
|
|
|
|
<div className="col-span-2 space-y-2 mt-2">
|
|
<div className="flex items-center justify-between px-1">
|
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">车辆明细 (前5台)</span>
|
|
<button
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onViewAll(target);
|
|
}}
|
|
className="text-[8px] text-blue-500 font-bold hover:underline"
|
|
>
|
|
查看全部
|
|
</button>
|
|
</div>
|
|
<div className="space-y-1">
|
|
{vehicles.slice(0, 5).map(vehicle => (
|
|
<div key={vehicle.plateNumber} className="bg-slate-50/50/50 px-2 py-1.5 rounded-lg flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-[10px] font-mono font-bold text-slate-700"><Blur>{vehicle.plateNumber}</Blur></span>
|
|
<span className="text-[7px] px-1 rounded bg-green-100 text-green-600 font-bold">
|
|
在线
|
|
</span>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="text-[10px] font-black text-blue-600">{vehicle.todayMileage}</span>
|
|
<span className="text-[8px] text-slate-400 ml-1">KM</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|