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