refactor(scheduling): simplify SwapPreview layout, remove verbose reason

- Remove type badge, reason section — too verbose
- Two clean white cards connected by arrow (swap diagram)
- Result section: predicted mileage, target, conclusion badge
- Tighter spacing, no redundant labels
- Professional tone, no childish wording

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-16 21:45:01 +08:00
parent 6a3a5ba319
commit 25199b507c

View File

@@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { ArrowDown, CheckCircle, Send, X } from 'lucide-react'; import { ArrowDownUp, CheckCircle, Send, X } from 'lucide-react';
import { motion } from 'motion/react';
import { sendNotify } from './api'; import { sendNotify } from './api';
import type { SchedulingSuggestion, CandidateVehicle } from './types'; import type { SchedulingSuggestion, CandidateVehicle } from './types';
import Blur from '../../components/Blur'; import Blur from '../../components/Blur';
@@ -25,151 +24,109 @@ export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSu
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [sent, setSent] = useState(false); const [sent, setSent] = useState(false);
const v = s.currentVehicle; const v = s.currentVehicle;
const isRescue = s.type === 'rescue_hopeless';
const handleSend = async () => { const handleSend = async () => {
if (sending || sent) return; if (sending || sent) return;
setSending(true); setSending(true);
try { try {
const result = await sendNotify({ const result = await sendNotify({ suggestionId: s.id, currentPlate: v.plateNumber, candidatePlate: c.plateNumber });
suggestionId: s.id, if (result.success) { setSent(true); onSuccess(); } else { alert(result.message || '发送失败'); }
currentPlate: v.plateNumber, } catch { alert('网络错误'); } finally { setSending(false); }
candidatePlate: c.plateNumber,
});
if (result.success) {
setSent(true);
onSuccess();
} else {
alert(result.message || '发送失败');
}
} catch {
alert('网络错误,请重试');
} finally {
setSending(false);
}
}; };
return ( return (
<div className="fixed inset-0 z-[80] bg-white flex flex-col"> <div className="fixed inset-0 z-[80] bg-[#F0F4F8] flex flex-col">
{/* Top bar */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 flex-shrink-0"> <div className="flex items-center justify-between px-5 py-3 bg-white border-b border-slate-200 flex-shrink-0">
<span className="text-sm font-bold text-slate-800"></span> <span className="text-sm font-bold text-slate-800"></span>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600 cursor-pointer"> <button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600 cursor-pointer"><X size={20} /></button>
<X size={20} />
</button>
</div> </div>
{/* Content — screenshot-friendly, no scroll needed */} {/* Content */}
<div className="flex-1 flex flex-col items-center justify-center px-6 py-4 gap-4 overflow-auto"> <div className="flex-1 overflow-auto px-5 py-5">
<div className="max-w-sm mx-auto space-y-4">
{/* Swap type badge */} {/* === Swap Cards === */}
<div className={`text-xs font-bold px-3 py-1 rounded-full ${ <div className="relative">
isRescue ? 'bg-blue-50 text-blue-700' : 'bg-orange-50 text-orange-700' {/* Current vehicle */}
}`}> <div className="bg-white rounded-2xl p-4 border border-slate-200 shadow-sm">
{isRescue ? '里程低·换走此车' : '里程高·换下此车'} <div className="flex items-start justify-between">
</div>
{/* === Swap Diagram === */}
<div className="w-full max-w-sm space-y-3">
{/* Current vehicle (换下/换走) */}
<div className={`rounded-2xl p-4 border ${isRescue ? 'bg-blue-50 border-blue-200' : 'bg-orange-50 border-orange-200'}`}>
<div className="text-[10px] font-bold text-slate-400 uppercase mb-2">
{isRescue ? '换走车辆' : '换下车辆'}
</div>
<div className="flex items-center justify-between">
<div> <div>
<div className="text-lg font-black text-slate-900 font-mono"><Blur>{v.plateNumber}</Blur></div> <div className="text-lg font-black text-slate-900 font-mono"><Blur>{v.plateNumber}</Blur></div>
<div className="text-[10px] text-slate-500 mt-0.5">{v.vehicleType} · {v.targetName}</div> <div className="text-[10px] text-slate-400 mt-0.5">{v.vehicleType} · {v.targetName}</div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="text-sm font-black text-slate-700">{fmtKm(v.currentYearMileage)} <span className="text-[9px] text-slate-400">km</span></div> <div className="text-base font-black text-slate-800">{fmtKm(v.currentYearMileage)}<span className="text-[9px] text-slate-400 ml-0.5">km</span></div>
<div className="text-[10px] text-slate-400"> {fmtKm(v.yearTarget)} km</div> <div className="text-[10px] text-slate-400"> {fmtKm(v.yearTarget)} km</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 mt-2 text-[10px] text-slate-500"> <div className="flex items-center gap-3 mt-2.5 text-[10px] text-slate-500">
<span> <b className="text-slate-700"><Blur>{v.customer || '-'}</Blur></b></span> <span><Blur>{v.customer || '-'}</Blur></span>
<span> <b className="text-slate-700">{Math.round(v.customerAvgDaily)}</b> km</span> <span> <b className="text-slate-700">{Math.round(v.customerAvgDaily)}</b></span>
<span> <b className={v.completionRate >= 1 ? 'text-emerald-600' : 'text-rose-500'}>{fmtRate(v.completionRate)}</b></span> <span> <b className={v.completionRate >= 1 ? 'text-emerald-600' : 'text-rose-500'}>{fmtRate(v.completionRate)}</b></span>
</div> </div>
</div> </div>
{/* Arrow */} {/* Arrow bridge */}
<div className="flex justify-center"> <div className="flex justify-center -my-3 relative z-10">
<div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center shadow-lg"> <div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center shadow-lg">
<ArrowDown size={18} className="text-white" /> <ArrowDownUp size={16} className="text-white" />
</div> </div>
</div> </div>
{/* Replacement vehicle (换上) */} {/* Replacement vehicle */}
<div className="rounded-2xl p-4 border bg-emerald-50 border-emerald-200"> <div className="bg-white rounded-2xl p-4 border border-emerald-300 shadow-sm">
<div className="text-[10px] font-bold text-slate-400 uppercase mb-2"> <div className="flex items-start justify-between">
</div>
<div className="flex items-center justify-between">
<div> <div>
<div className="text-lg font-black text-slate-900 font-mono"><Blur>{c.plateNumber}</Blur></div> <div className="text-lg font-black text-slate-900 font-mono"><Blur>{c.plateNumber}</Blur></div>
<div className="text-[10px] text-slate-500 mt-0.5">{c.vehicleType} · {c.targetName || '库存'}</div> <div className="text-[10px] text-slate-400 mt-0.5">{c.vehicleType} · {c.targetName || '库存'} · {c.region}</div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="text-sm font-black text-slate-700">{fmtKm(c.totalMileage)} <span className="text-[9px] text-slate-400">km</span></div> <div className="text-base font-black text-slate-800">{fmtKm(c.totalMileage)}<span className="text-[9px] text-slate-400 ml-0.5">km</span></div>
<div className="text-[10px] text-slate-400"> {c.yearTarget ? fmtKm(c.yearTarget) : '-'} km</div> <div className="text-[10px] text-slate-400"> {c.yearTarget ? fmtKm(c.yearTarget) : '-'} km</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 mt-2 text-[10px] text-slate-500"> <div className="flex items-center gap-3 mt-2.5 text-[10px] text-slate-500">
<span> <b className="text-rose-500">{fmtKm(c.mileageGap)}</b> km</span> <span> <b className="text-rose-500">{fmtKm(c.mileageGap)}</b></span>
<span> <b className="text-slate-700">{c.region}</b></span>
</div> </div>
</div> </div>
</div> </div>
{/* === Reason === */} {/* === Result === */}
<div className="w-full max-w-sm bg-slate-50 rounded-xl p-3 border border-slate-200"> <div className="bg-white rounded-2xl p-4 border border-slate-200 shadow-sm">
<div className="text-[10px] font-bold text-slate-500 uppercase mb-1"></div> <div className="text-[10px] font-bold text-slate-400 uppercase mb-3"></div>
<div className="text-xs text-slate-700 leading-relaxed whitespace-pre-line">{s.reason}</div> <div className="flex items-end gap-6">
</div>
{/* === Conclusion === */}
<div className="w-full max-w-sm bg-emerald-50 rounded-xl p-3 border border-emerald-200">
<div className="text-[10px] font-bold text-emerald-600 uppercase mb-1"></div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-600">
<Blur>{c.plateNumber}</Blur> <Blur>{v.customer || '-'}</Blur>
</span>
</div>
<div className="flex items-center gap-4 mt-2">
<div> <div>
<div className="text-[9px] text-slate-400"></div> <div className="text-[9px] text-slate-400 mb-0.5"></div>
<div className="text-base font-black text-emerald-700">{fmtKm(c.predictedAfterSwap)} km</div> <div className="text-xl font-black text-slate-800">{fmtKm(c.predictedAfterSwap)} <span className="text-[10px] font-normal text-slate-400">km</span></div>
</div> </div>
<div> <div>
<div className="text-[9px] text-slate-400"></div> <div className="text-[9px] text-slate-400 mb-0.5"></div>
<div className="text-base font-black text-slate-700">{c.yearTarget ? fmtKm(c.yearTarget) : '-'} km</div> <div className="text-xl font-black text-slate-800">{c.yearTarget ? fmtKm(c.yearTarget) : '-'} <span className="text-[10px] font-normal text-slate-400">km</span></div>
</div> </div>
<div> <div className={`text-sm font-black px-3 py-1 rounded-lg ${c.canQualifyAfterSwap ? 'bg-emerald-50 text-emerald-600' : 'bg-amber-50 text-amber-600'}`}>
<div className="text-[9px] text-slate-400"></div>
<div className={`text-base font-black ${c.canQualifyAfterSwap ? 'text-emerald-600' : 'text-amber-600'}`}>
{c.canQualifyAfterSwap ? '可达标' : '需关注'} {c.canQualifyAfterSwap ? '可达标' : '需关注'}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Bottom action */} {/* Bottom */}
<div className="px-6 pb-6 pt-2 flex-shrink-0"> <div className="px-5 pb-6 pt-2 flex-shrink-0 bg-[#F0F4F8]">
<div className="max-w-sm mx-auto">
<button <button
onClick={handleSend} onClick={handleSend}
disabled={sending || sent} disabled={sending || sent}
className={`w-full flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-bold transition-all cursor-pointer ${ className={`w-full flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-bold transition-all cursor-pointer ${
sent sent ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-800 hover:bg-slate-900 text-white active:scale-[0.98] shadow-lg'
? 'bg-emerald-100 text-emerald-600'
: 'bg-slate-800 hover:bg-slate-900 text-white active:scale-[0.98] shadow-lg'
}`} }`}
> >
{sent ? <><CheckCircle size={16} /> </> : <><Send size={16} /> </>} {sent ? <><CheckCircle size={16} /> </> : <><Send size={16} /> </>}
</button> </button>
</div> </div>
</div> </div>
</div>
); );
} }