feat: 实现里程管理统计报表视图(1:1 复刻原型)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,554 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
ResponsiveContainer, LineChart, Line, AreaChart, Area,
|
||||
Cell, LabelList,
|
||||
} from 'recharts';
|
||||
import {
|
||||
Truck, ChevronDown, Maximize2, Minimize2,
|
||||
Search, ArrowUpDown, X,
|
||||
} from 'lucide-react';
|
||||
import type { TargetSummary, TargetVehicle, TrendPoint } from './types';
|
||||
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
|
||||
|
||||
export default function StatisticsView() {
|
||||
return <div>StatisticsView placeholder</div>;
|
||||
const [targets, setTargets] = useState<TargetSummary[]>([]);
|
||||
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
|
||||
const [targetVehiclesMap, setTargetVehiclesMap] = useState<Record<number, TargetVehicle[]>>({});
|
||||
const [selectedTargetId, setSelectedTargetId] = useState<number | null>(null);
|
||||
|
||||
const [chartType, setChartType] = useState<'bar' | 'line' | 'area'>('bar');
|
||||
const [isTableFullscreen, setIsTableFullscreen] = useState(false);
|
||||
const [expandedModel, setExpandedModel] = useState<string | null>(null);
|
||||
const [viewAllTargetId, setViewAllTargetId] = useState<number | null>(null);
|
||||
const [viewAllTargetName, setViewAllTargetName] = useState<string>('');
|
||||
const [viewAllSearch, setViewAllSearch] = useState('');
|
||||
const [viewAllSort, setViewAllSort] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
// Load targets on mount
|
||||
useEffect(() => {
|
||||
fetchTargets().then(data => {
|
||||
setTargets(data);
|
||||
if (data.length > 0 && !selectedTargetId) {
|
||||
setSelectedTargetId(data[0].id);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Load trend when selectedTargetId changes
|
||||
useEffect(() => {
|
||||
if (selectedTargetId === null) return;
|
||||
fetchTrend(selectedTargetId).then(setTrendData).catch(() => setTrendData([]));
|
||||
}, [selectedTargetId]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pb-2 landscape:pb-4 landscape:h-full landscape:overflow-hidden landscape:flex landscape:flex-col flex-none landscape:flex-1">
|
||||
{/* Project Selector - Full width even in landscape */}
|
||||
<div className="bg-white landscape:bg-slate-900/50 landscape:border-slate-800 p-2 rounded-2xl shadow-sm border border-slate-100 flex gap-1 overflow-x-auto no-scrollbar flex-shrink-0">
|
||||
{targets.map(target => (
|
||||
<button
|
||||
key={target.id}
|
||||
onClick={() => setSelectedTargetId(target.id)}
|
||||
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
selectedTargetId === target.id
|
||||
? 'bg-blue-600 text-white shadow-md shadow-blue-200'
|
||||
: 'bg-slate-50 landscape:bg-slate-800 text-slate-500 landscape:text-slate-400 hover:bg-slate-100 landscape:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
{target.targetName}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col landscape:flex-row gap-4 flex-1 overflow-hidden">
|
||||
{/* Left Side: Trend Chart / Dashboard Sidebar */}
|
||||
<div className="flex-none landscape:flex-1 landscape:w-2/3 space-y-4 flex flex-col overflow-y-auto no-scrollbar">
|
||||
{/* KPI Cards in Landscape */}
|
||||
<div className="hidden landscape:grid grid-cols-4 gap-4 flex-shrink-0">
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">今日总里程</div>
|
||||
<div className="text-xl font-black text-white tracking-tighter">
|
||||
{targets.reduce((sum, t) => sum + t.todayTotal, 0).toLocaleString()}
|
||||
<span className="text-blue-400 text-[10px] ml-1">KM</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">累计总里程</div>
|
||||
<div className="text-xl font-black text-white tracking-tighter">
|
||||
{targets.reduce((sum, t) => sum + t.cumulativeTotal, 0).toLocaleString()}
|
||||
<span className="text-blue-400 text-[10px] ml-1">KM</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">总考核车辆</div>
|
||||
<div className="text-xl font-black text-white tracking-tighter">
|
||||
{targets.reduce((sum, t) => sum + t.vehicleCount, 0)}
|
||||
<span className="text-blue-400 text-[10px] ml-1">台</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">平均完成率</div>
|
||||
<div className="text-xl font-black text-white tracking-tighter">
|
||||
{targets.length > 0 ? (targets.reduce((sum, t) => sum + t.avgCompletion, 0) / targets.length).toFixed(1) : '0.0'}
|
||||
<span className="text-blue-400 text-[10px] ml-1">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white landscape:bg-slate-900/40 landscape:border-slate-800 p-4 rounded-2xl shadow-sm border border-slate-100 flex-1 flex flex-col min-h-[300px]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-4 bg-blue-600 rounded-full" />
|
||||
<h3 className="text-sm font-bold text-slate-800 landscape:text-white">7天里程趋势</h3>
|
||||
</div>
|
||||
<div className="flex bg-slate-50 landscape:bg-slate-800 p-1 rounded-lg">
|
||||
{(['bar', 'line', 'area'] as const).map(type => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setChartType(type)}
|
||||
className={`px-2 py-1 rounded-md text-[10px] font-bold transition-all ${
|
||||
chartType === type ? 'bg-white landscape:bg-slate-700 text-blue-600 landscape:text-blue-400 shadow-sm' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
{type === 'bar' ? '柱状' : type === 'line' ? '折线' : '面积'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 w-full min-h-[250px] relative">
|
||||
<div className="absolute inset-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{chartType === 'bar' ? (
|
||||
<BarChart data={trendData} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" strokeOpacity={0.1} />
|
||||
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} dy={10} tickFormatter={(val) => val.split('-')[1]} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} />
|
||||
<Tooltip cursor={{ fill: '#f8fafc', fillOpacity: 0.1 }} contentStyle={{ borderRadius: '12px', border: 'none', backgroundColor: '#1e293b', color: '#fff', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)', fontSize: '10px' }} />
|
||||
<Bar dataKey="mileage" fill="#3b82f6" radius={[4, 4, 0, 0]} barSize={20}>
|
||||
<LabelList dataKey="mileage" position="top" style={{ fontSize: 10, fill: '#64748b', fontWeight: 'bold' }} />
|
||||
{trendData.map((_entry: TrendPoint, index: number) => (
|
||||
<Cell key={`cell-${index}`} fill={index === trendData.length - 1 ? '#2563eb' : '#60a5fa'} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
) : chartType === 'line' ? (
|
||||
<LineChart data={trendData} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" strokeOpacity={0.1} />
|
||||
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} dy={10} tickFormatter={(val) => val.split('-')[1]} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} />
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', backgroundColor: '#1e293b', color: '#fff', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)', fontSize: '10px' }} />
|
||||
<Line type="monotone" dataKey="mileage" stroke="#3b82f6" strokeWidth={3} dot={{ r: 4, fill: '#3b82f6' }}>
|
||||
<LabelList dataKey="mileage" position="top" offset={10} style={{ fontSize: 10, fill: '#64748b', fontWeight: 'bold' }} />
|
||||
</Line>
|
||||
</LineChart>
|
||||
) : (
|
||||
<AreaChart data={trendData} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorMileage" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" strokeOpacity={0.1} />
|
||||
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} dy={10} tickFormatter={(val) => val.split('-')[1]} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} />
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', backgroundColor: '#1e293b', color: '#fff', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)', fontSize: '10px' }} />
|
||||
<Area type="monotone" dataKey="mileage" stroke="#3b82f6" fillOpacity={1} fill="url(#colorMileage)">
|
||||
<LabelList dataKey="mileage" position="top" offset={10} style={{ fontSize: 10, fill: '#64748b', fontWeight: 'bold' }} />
|
||||
</Area>
|
||||
</AreaChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Summary Section */}
|
||||
<div className="w-full landscape:w-1/3 flex-shrink-0 space-y-2 flex flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-between px-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-4 bg-blue-600 rounded-full" />
|
||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-widest">车型考核里程汇总</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsTableFullscreen(true)}
|
||||
className="p-1.5 bg-white landscape:bg-slate-800 text-slate-400 rounded-lg border border-slate-100 landscape:border-slate-700 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">
|
||||
{targets.map((target, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white landscape:bg-slate-900/50 px-3 py-2 rounded-xl border border-slate-50 landscape:border-slate-800 shadow-sm flex flex-col active:bg-slate-50 landscape:active:bg-slate-800 transition-all cursor-pointer"
|
||||
onClick={() => {
|
||||
const name = target.targetName;
|
||||
setExpandedModel(expandedModel === name ? null : name);
|
||||
if (!targetVehiclesMap[target.id]) {
|
||||
fetchTargetVehicles(target.id).then(data => {
|
||||
setTargetVehiclesMap(prev => ({ ...prev, [target.id]: data }));
|
||||
}).catch(() => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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 landscape:bg-slate-800 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 landscape:text-white">{target.targetName}</span>
|
||||
<span className="text-[8px] px-1 rounded bg-blue-50 landscape:bg-blue-900/30 text-blue-600 landscape:text-blue-400 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">完成率:</span>
|
||||
<span className={`text-[9px] font-bold ${target.avgCompletion >= 90 ? 'text-emerald-500' : 'text-blue-500'}`}>{target.avgCompletion}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[9px] text-slate-400">达标:</span>
|
||||
<span className="text-[9px] font-bold text-slate-600 landscape:text-slate-400">{target.yearQualifiedCount}台</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 landscape:text-white leading-none mb-0.5">
|
||||
{target.todayTotal.toLocaleString()} <span className="text-[8px] text-slate-300 font-bold uppercase">KM</span>
|
||||
</div>
|
||||
<div className="text-[8px] font-bold text-slate-300">
|
||||
累计: {target.cumulativeTotal.toLocaleString()} KM
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
animate={{ rotate: expandedModel === target.targetName ? 180 : 0 }}
|
||||
className="text-slate-300"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{expandedModel === target.targetName && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="pt-3 mt-2 border-t border-slate-50 landscape:border-slate-800 grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<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 landscape:text-slate-300">{target.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 landscape:text-slate-300">{(target.totalMileagePerVehicle * target.vehicleCount).toLocaleString()} KM</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 landscape:text-slate-300">{target.annualMileagePerVehicle.toLocaleString()} KM</p>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">50%达标数</p>
|
||||
<p className="text-[10px] font-black text-blue-600 landscape:text-blue-400">{target.halfQualifiedCount} 台</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 landscape:text-slate-300">{target.currentYearTarget.toLocaleString()} KM</p>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">已完成(截止3.31)</p>
|
||||
<p className="text-[10px] font-black text-emerald-600">{target.currentYearCompleted.toLocaleString()} KM</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-rose-500">{target.remaining.toLocaleString()} KM</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-blue-500">{target.dailyTarget} KM</p>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center justify-between bg-slate-50 landscape:bg-slate-800 p-2 rounded-lg">
|
||||
<span className="text-[9px] font-bold text-slate-500">剩余考核天数</span>
|
||||
<span className="text-[10px] font-black text-slate-900 landscape:text-white">{target.daysLeft} 天</span>
|
||||
</div>
|
||||
|
||||
{/* Vehicle List Detail */}
|
||||
<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={(e) => {
|
||||
e.stopPropagation();
|
||||
setViewAllTargetId(target.id);
|
||||
setViewAllTargetName(target.targetName);
|
||||
if (!targetVehiclesMap[target.id]) {
|
||||
fetchTargetVehicles(target.id).then(data => {
|
||||
setTargetVehiclesMap(prev => ({ ...prev, [target.id]: data }));
|
||||
}).catch(() => {});
|
||||
}
|
||||
}}
|
||||
className="text-[8px] text-blue-500 font-bold hover:underline"
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{(targetVehiclesMap[target.id] || []).slice(0, 5).map(tv => (
|
||||
<div key={tv.plateNumber} className="bg-slate-50/50 landscape:bg-slate-800/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 landscape:text-slate-300">{tv.plateNumber}</span>
|
||||
<span className="text-[7px] px-1 rounded bg-green-100 landscape:bg-green-900/30 text-green-600 landscape:text-green-400 font-bold">
|
||||
在线
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] font-black text-blue-600 landscape:text-blue-400">{tv.todayMileage}</span>
|
||||
<span className="text-[8px] text-slate-400 ml-1">KM</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fullscreen Table Overlay */}
|
||||
<AnimatePresence>
|
||||
{isTableFullscreen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[100] bg-slate-950 flex flex-col p-4 landscape:flex-row gap-4 overflow-hidden"
|
||||
>
|
||||
{/* Sidebar with KPI Cards */}
|
||||
<div className="flex flex-col gap-4 w-full landscape:w-72 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-1 h-6 bg-blue-500 rounded-full"></div>
|
||||
<h2 className="text-white font-bold text-lg">车型考核汇总</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsTableFullscreen(false)}
|
||||
className="p-2 bg-slate-800 text-slate-400 rounded-full hover:text-white transition-colors"
|
||||
>
|
||||
<Minimize2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 landscape:grid-cols-1 gap-3">
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">今日总里程</div>
|
||||
<div className="text-2xl font-black text-white tracking-tighter">
|
||||
{targets.reduce((sum, t) => sum + t.todayTotal, 0).toLocaleString()}
|
||||
<span className="text-blue-400 text-xs ml-2">KM</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">累计总里程</div>
|
||||
<div className="text-2xl font-black text-white tracking-tighter">
|
||||
{targets.reduce((sum, t) => sum + t.cumulativeTotal, 0).toLocaleString()}
|
||||
<span className="text-blue-400 text-xs ml-2">KM</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">总考核车辆</div>
|
||||
<div className="text-2xl font-black text-white tracking-tighter">
|
||||
{targets.reduce((sum, t) => sum + t.vehicleCount, 0)}
|
||||
<span className="text-blue-400 text-xs ml-2">台</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1">平均完成率</div>
|
||||
<div className="text-2xl font-black text-white tracking-tighter">
|
||||
{targets.length > 0 ? (targets.reduce((sum, t) => sum + t.avgCompletion, 0) / targets.length).toFixed(1) : '0.0'}
|
||||
<span className="text-blue-400 text-xs ml-2">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-slate-900/30 border border-slate-800 rounded-2xl overflow-hidden flex flex-col">
|
||||
<div className="p-4 border-b border-slate-800 flex justify-between items-center bg-slate-900/50">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">车型考核明细数据</span>
|
||||
<span className="text-[10px] text-slate-500">最后更新: {new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-left border-collapse min-w-[1200px]">
|
||||
<thead className="sticky top-0 bg-slate-900 z-10">
|
||||
<tr className="border-b border-slate-800">
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase sticky left-0 bg-slate-900 z-10">车型</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">车辆数</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">总考核里程</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">已行驶总里程</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">总完成率</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">考核区间</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">年考核任务/辆</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase text-emerald-400">达标车辆数</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase text-blue-400">50%达标数</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase text-white">今日总里程</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">本年需完成</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">已完成(截止3.31)</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase text-rose-400">未完成总数</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">剩余天数</th>
|
||||
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase text-blue-400">日均需完成</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{targets.map((target, idx) => (
|
||||
<tr key={idx} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="p-4 text-sm font-bold text-white sticky left-0 bg-slate-900 z-10 border-r border-slate-800">{target.targetName}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{target.vehicleCount}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{(target.totalMileagePerVehicle * target.vehicleCount).toLocaleString()}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{target.cumulativeTotal.toLocaleString()}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden min-w-[60px]">
|
||||
<div
|
||||
className={`h-full rounded-full ${target.avgCompletion >= 90 ? 'bg-emerald-500' : target.avgCompletion >= 80 ? 'bg-blue-500' : 'bg-amber-500'}`}
|
||||
style={{ width: `${target.avgCompletion}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-white">{target.avgCompletion}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-[10px] text-slate-400">{target.period}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{target.annualMileagePerVehicle}</td>
|
||||
<td className="p-4 text-xs font-bold text-emerald-400">{target.yearQualifiedCount}</td>
|
||||
<td className="p-4 text-xs font-bold text-blue-400">{target.halfQualifiedCount}</td>
|
||||
<td className="p-4 text-xs font-bold text-white">{target.todayTotal.toLocaleString()}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{target.currentYearTarget.toLocaleString()}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{target.currentYearCompleted.toLocaleString()}</td>
|
||||
<td className="p-4 text-xs font-bold text-rose-400">{target.remaining.toLocaleString()}</td>
|
||||
<td className="p-4 text-xs text-slate-300">{target.daysLeft}</td>
|
||||
<td className="p-4 text-xs font-bold text-blue-400">{target.dailyTarget}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* View All Vehicles Side Panel */}
|
||||
<AnimatePresence>
|
||||
{viewAllTargetId !== null && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setViewAllTargetId(null)}
|
||||
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">{viewAllTargetName}</h3>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">车辆实时明细清单</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setViewAllTargetId(null);
|
||||
setViewAllSearch('');
|
||||
}}
|
||||
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-4 border-b border-slate-50 space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索车牌号..."
|
||||
value={viewAllSearch}
|
||||
onChange={(e) => setViewAllSearch(e.target.value)}
|
||||
className="w-full pl-9 pr-4 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="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">排序方式: 今日里程</span>
|
||||
<button
|
||||
onClick={() => setViewAllSort(prev => prev === '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} />
|
||||
{viewAllSort === 'desc' ? '降序' : '升序'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2 no-scrollbar">
|
||||
{(viewAllTargetId !== null ? (targetVehiclesMap[viewAllTargetId] || []) : []).filter(tv =>
|
||||
tv.plateNumber.toLowerCase().includes(viewAllSearch.toLowerCase())
|
||||
).sort((a, b) => {
|
||||
const valA = a.todayMileage || 0;
|
||||
const valB = b.todayMileage || 0;
|
||||
return viewAllSort === 'desc' ? valB - valA : valA - valB;
|
||||
}).map(tv => (
|
||||
<div key={tv.plateNumber} className="bg-slate-50 p-4 rounded-2xl border border-slate-100 flex items-center justify-between group hover:border-blue-200 transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-white border border-slate-100 flex items-center justify-center shadow-sm">
|
||||
<Truck size={18} className="text-slate-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-black text-slate-900 font-mono">{tv.plateNumber}</span>
|
||||
<span className="text-[8px] px-1.5 py-0.5 rounded-full font-bold bg-green-100 text-green-600">
|
||||
在线
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 mt-0.5"> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-black text-blue-600">{tv.todayMileage} <span className="text-[9px] text-slate-400">KM</span></div>
|
||||
<div className="text-[9px] font-bold text-slate-400 mt-0.5">累计: {tv.totalMileage?.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-50 border-t border-slate-100">
|
||||
<button
|
||||
onClick={() => setViewAllTargetId(null)}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user