feat(scheduling): rename 完成→年度达标, add sort by 客户日均/年度达标 to list
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-04-16 23:03:59 +08:00
parent 8664317852
commit 9f781c766a

View File

@@ -1,4 +1,5 @@
import { ArrowRightLeft, ChevronRight } from 'lucide-react'; import { useState, useMemo } from 'react';
import { ArrowRightLeft, ChevronRight, ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import type { SchedulingSuggestion } from './types'; import type { SchedulingSuggestion } from './types';
import Blur from '../../components/Blur'; import Blur from '../../components/Blur';
@@ -12,7 +13,27 @@ function fmtRate(rate: number): string {
return (rate * 100).toFixed(1) + '%'; return (rate * 100).toFixed(1) + '%';
} }
type SortKey = 'default' | 'avgDaily' | 'completion';
type SortDir = 'asc' | 'desc';
export default function SuggestionList({ suggestions, onSelect }: Props) { export default function SuggestionList({ suggestions, onSelect }: Props) {
const [sortKey, setSortKey] = useState<SortKey>('default');
const [sortDir, setSortDir] = useState<SortDir>('desc');
const toggleSort = (key: SortKey) => {
if (sortKey === key) { setSortDir(d => d === 'desc' ? 'asc' : 'desc'); }
else { setSortKey(key); setSortDir('desc'); }
};
const sorted = useMemo(() => {
if (sortKey === 'default') return suggestions;
return [...suggestions].sort((a, b) => {
const va = sortKey === 'avgDaily' ? a.currentVehicle.customerAvgDaily : a.currentVehicle.completionRate;
const vb = sortKey === 'avgDaily' ? b.currentVehicle.customerAvgDaily : b.currentVehicle.completionRate;
return sortDir === 'desc' ? vb - va : va - vb;
});
}, [suggestions, sortKey, sortDir]);
if (suggestions.length === 0) { if (suggestions.length === 0) {
return ( return (
<div className="py-16 text-center"> <div className="py-16 text-center">
@@ -23,55 +44,81 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
} }
return ( return (
<div className="divide-y divide-slate-50"> <div>
{suggestions.map((s, idx) => { {/* Sort controls */}
const isRescue = s.type === 'rescue_hopeless'; <div className="px-4 py-2 border-b border-slate-50 flex items-center gap-2">
const v = s.currentVehicle; <button
onClick={() => toggleSort('avgDaily')}
className={`text-[10px] px-2 py-1 rounded-lg border flex items-center gap-1 cursor-pointer transition-colors ${
sortKey === 'avgDaily' ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-500'
}`}
>
{sortKey === 'avgDaily' && (sortDir === 'desc' ? <ArrowDown size={10} /> : <ArrowUp size={10} />)}
{sortKey !== 'avgDaily' && <ArrowUpDown size={10} />}
</button>
<button
onClick={() => toggleSort('completion')}
className={`text-[10px] px-2 py-1 rounded-lg border flex items-center gap-1 cursor-pointer transition-colors ${
sortKey === 'completion' ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-500'
}`}
>
{sortKey === 'completion' && (sortDir === 'desc' ? <ArrowDown size={10} /> : <ArrowUp size={10} />)}
{sortKey !== 'completion' && <ArrowUpDown size={10} />}
</button>
</div>
return ( <div className="divide-y divide-slate-50">
<motion.div {sorted.map((s, idx) => {
key={s.id} const isRescue = s.type === 'rescue_hopeless';
initial={{ opacity: 0 }} const v = s.currentVehicle;
animate={{ opacity: 1 }}
transition={{ delay: Math.min(idx * 0.02, 0.3) }}
className="px-4 py-3 hover:bg-slate-50/60 cursor-pointer transition-colors active:bg-slate-100 flex items-center gap-3"
onClick={() => onSelect(s)}
>
{/* Color bar */}
<div className={`w-1 h-10 rounded-full flex-shrink-0 ${isRescue ? 'bg-blue-400' : 'bg-amber-400'}`} />
{/* Info */} return (
<div className="flex-1 min-w-0"> <motion.div
<div className="flex items-center gap-2"> key={s.id}
<span className="text-xs font-black text-slate-900 font-mono"> initial={{ opacity: 0 }}
<Blur>{v.plateNumber}</Blur> animate={{ opacity: 1 }}
</span> transition={{ delay: Math.min(idx * 0.02, 0.3) }}
<span className={`text-[9px] px-1.5 py-px rounded font-bold ${ className="px-4 py-3 hover:bg-slate-50/60 cursor-pointer transition-colors active:bg-slate-100 flex items-center gap-3"
isRescue ? 'bg-blue-50 text-blue-600' : 'bg-amber-50 text-amber-600' onClick={() => onSelect(s)}
}`}> >
{isRescue ? '里程低·换走' : '里程高·换下'} {/* Color bar */}
</span> <div className={`w-1 h-10 rounded-full flex-shrink-0 ${isRescue ? 'bg-blue-400' : 'bg-amber-400'}`} />
<span className="text-[9px] text-slate-400">{v.vehicleType}</span>
<span className="text-[9px] text-slate-300">·</span> {/* Info */}
<span className="text-[9px] text-slate-400">{v.region}</span> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-black text-slate-900 font-mono">
<Blur>{v.plateNumber}</Blur>
</span>
<span className={`text-[9px] px-1.5 py-px rounded font-bold ${
isRescue ? 'bg-blue-50 text-blue-600' : 'bg-amber-50 text-amber-600'
}`}>
{isRescue ? '里程低·换走' : '里程高·换下'}
</span>
<span className="text-[9px] text-slate-400">{v.vehicleType}</span>
<span className="text-[9px] text-slate-300">·</span>
<span className="text-[9px] text-slate-400">{v.region}</span>
</div>
<div className="flex items-center gap-2 mt-0.5 text-[10px] text-slate-400 overflow-hidden">
{v.department && <span className="flex-shrink-0 text-slate-500">{v.department}</span>}
{v.manager && <span className="flex-shrink-0 text-slate-500">{v.manager}</span>}
<span className="truncate max-w-[35%] flex-shrink"><Blur>{v.customer || '-'}</Blur></span>
<span className="flex-shrink-0"> <span className="text-slate-600 font-medium">{Math.round(v.customerAvgDaily)}</span></span>
<span className="flex-shrink-0"> <span className={`font-medium ${v.completionRate >= 1 ? 'text-emerald-600' : v.completionRate >= 0.5 ? 'text-amber-600' : 'text-rose-500'}`}>{fmtRate(v.completionRate)}</span></span>
</div>
</div> </div>
<div className="flex items-center gap-2 mt-0.5 text-[10px] text-slate-400 overflow-hidden">
{v.department && <span className="flex-shrink-0 text-slate-500">{v.department}</span>}
{v.manager && <span className="flex-shrink-0 text-slate-500">{v.manager}</span>}
<span className="truncate max-w-[35%] flex-shrink"><Blur>{v.customer || '-'}</Blur></span>
<span className="flex-shrink-0"> <span className="text-slate-600 font-medium">{Math.round(v.customerAvgDaily)}</span></span>
<span className="flex-shrink-0"> <span className={`font-medium ${v.completionRate >= 1 ? 'text-emerald-600' : v.completionRate >= 0.5 ? 'text-amber-600' : 'text-rose-500'}`}>{fmtRate(v.completionRate)}</span></span>
</div>
</div>
{/* Right */} {/* Right */}
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
<span className="text-[9px] text-slate-400"></span> <span className="text-[9px] text-slate-400"></span>
<ChevronRight size={14} className="text-slate-300" /> <ChevronRight size={14} className="text-slate-300" />
</div> </div>
</motion.div> </motion.div>
); );
})} })}
</div>
</div> </div>
); );
} }