diff --git a/src/modules/scheduling/SuggestionDetail.tsx b/src/modules/scheduling/SuggestionDetail.tsx index c2f046d..37ce573 100644 --- a/src/modules/scheduling/SuggestionDetail.tsx +++ b/src/modules/scheduling/SuggestionDetail.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; import { - X, MapPin, AlertTriangle, CheckCircle, Send, ArrowDown, ArrowUp, + X, MapPin, AlertTriangle, CheckCircle, ArrowDown, ArrowUp, ArrowRight, } from 'lucide-react'; import { motion } from 'motion/react'; -import { sendNotify } from './api'; import type { SchedulingSuggestion, CandidateVehicle } from './types'; import Blur from '../../components/Blur'; +import SwapPreview from './SwapPreview'; interface Props { suggestion: SchedulingSuggestion; @@ -23,34 +23,12 @@ function fmtRate(rate: number): string { } export default function SuggestionDetail({ suggestion: s, onClose, onNotifySuccess }: Props) { - const [sending, setSending] = useState(false); + const [previewCandidate, setPreviewCandidate] = useState(null); const [sentPlates, setSentPlates] = useState>(new Set()); const v = s.currentVehicle; const isRescue = s.type === 'rescue_hopeless'; - const handleNotify = async (candidate: CandidateVehicle) => { - if (sending || sentPlates.has(candidate.plateNumber)) return; - setSending(true); - try { - const result = await sendNotify({ - suggestionId: s.id, - currentPlate: v.plateNumber, - candidatePlate: candidate.plateNumber, - }); - if (result.success) { - setSentPlates(prev => new Set(prev).add(candidate.plateNumber)); - onNotifySuccess(); - } else { - alert(result.message || '发送失败'); - } - } catch { - alert('网络错误,请重试'); - } finally { - setSending(false); - } - }; - return (
@@ -206,6 +184,20 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce + + {/* Full-screen swap preview */} + {previewCandidate && ( + setPreviewCandidate(null)} + onSuccess={() => { + setSentPlates(prev => new Set(prev).add(previewCandidate.plateNumber)); + setPreviewCandidate(null); + onNotifySuccess(); + }} + /> + )} ); } diff --git a/src/modules/scheduling/SwapPreview.tsx b/src/modules/scheduling/SwapPreview.tsx new file mode 100644 index 0000000..cd70d03 --- /dev/null +++ b/src/modules/scheduling/SwapPreview.tsx @@ -0,0 +1,175 @@ +import { useState } from 'react'; +import { ArrowDown, CheckCircle, Send, X } from 'lucide-react'; +import { motion } from 'motion/react'; +import { sendNotify } from './api'; +import type { SchedulingSuggestion, CandidateVehicle } from './types'; +import Blur from '../../components/Blur'; + +interface Props { + suggestion: SchedulingSuggestion; + candidate: CandidateVehicle; + onClose: () => void; + onSuccess: () => void; +} + +function fmtKm(value: number): string { + if (value >= 10000) return (value / 10000).toFixed(1) + '万'; + return value.toLocaleString(); +} + +function fmtRate(rate: number): string { + return (rate * 100).toFixed(1) + '%'; +} + +export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSuccess }: Props) { + 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); + } + }; + + return ( +
+ {/* Top bar */} +
+ 车辆替换方案 + +
+ + {/* Content — screenshot-friendly, no scroll needed */} +
+ + {/* Swap type badge */} +
+ {isRescue ? '里程低·换走此车' : '里程高·换下此车'} +
+ + {/* === Swap Diagram === */} +
+ + {/* Current vehicle (换下/换走) */} +
+
+ {isRescue ? '换走车辆' : '换下车辆'} +
+
+
+
{v.plateNumber}
+
{v.vehicleType} · {v.targetName}
+
+
+
{fmtKm(v.currentYearMileage)} km
+
考核 {fmtKm(v.yearTarget)} km
+
+
+
+ 客户 {v.customer || '-'} + 日均 {Math.round(v.customerAvgDaily)} km + 完成 = 1 ? 'text-emerald-600' : 'text-rose-500'}>{fmtRate(v.completionRate)} +
+
+ + {/* Arrow */} +
+
+ +
+
+ + {/* Replacement vehicle (换上) */} +
+
+ 换上车辆 +
+
+
+
{c.plateNumber}
+
{c.vehicleType} · {c.targetName || '库存'}
+
+
+
{fmtKm(c.totalMileage)} km
+
考核 {c.yearTarget ? fmtKm(c.yearTarget) : '-'} km
+
+
+
+ 缺口 {fmtKm(c.mileageGap)} km + 区域 {c.region} +
+
+
+ + {/* === Reason === */} +
+
替换原因
+
{s.reason}
+
+ + {/* === Conclusion === */} +
+
替换后预测
+
+ + {c.plateNumber} 换到客户「{v.customer || '-'}」后 + +
+
+
+
预测年终
+
{fmtKm(c.predictedAfterSwap)} km
+
+
+
考核目标
+
{c.yearTarget ? fmtKm(c.yearTarget) : '-'} km
+
+
+
结论
+
+ {c.canQualifyAfterSwap ? '可达标' : '需关注'} +
+
+
+
+
+ + {/* Bottom action */} +
+ +
+
+ ); +}