Files
ln-bi/src/modules/mileage/statistics/TargetDetailPanel.tsx
T
shishengliang 6ea1b3d5dd
ci/woodpecker/push/woodpecker Pipeline failed
revert: restore v1.1.14 (37a9f303)
2026-09-16 09:29:30 +08:00

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>
);
}