diff --git a/src/modules/scheduling/SuggestionDetail.tsx b/src/modules/scheduling/SuggestionDetail.tsx new file mode 100644 index 0000000..afbec72 --- /dev/null +++ b/src/modules/scheduling/SuggestionDetail.tsx @@ -0,0 +1,248 @@ +import { useState } from 'react'; +import { + X, + Truck, + MapPin, + AlertTriangle, + CheckCircle, + Send, +} 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; + onClose: () => void; + onNotifySuccess: () => void; +} + +function fmtKm(value: number): string { + if (value >= 10000) return (value / 10000).toFixed(1) + '万'; + return value.toLocaleString(); +} + +export default function SuggestionDetail({ suggestion: s, onClose, onNotifySuccess }: Props) { + const [sending, setSending] = useState(false); + const [sentPlates, setSentPlates] = useState>(new Set()); + + const v = s.currentVehicle; + const isRescue = s.type === 'rescue_hopeless'; + + const headerBg = isRescue ? 'bg-rose-600' : 'bg-amber-600'; + const title = isRescue ? '智能调度干预 — 抢救低里程' : '智能调度干预 — 释放已达标'; + + const cardBg = isRescue + ? 'bg-rose-50 border border-rose-100' + : 'bg-amber-50 border border-amber-100'; + + const completionColor = + v.completionRate >= 1 + ? 'text-emerald-600' + : v.completionRate >= 0.6 + ? 'text-amber-600' + : 'text-rose-600'; + + 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 (e) { + alert('网络错误,请重试'); + } finally { + setSending(false); + } + }; + + return ( +
+ + {/* Header */} +
+ {title} + +
+ + {/* Scrollable body */} +
+ {/* Current Vehicle Card */} +
+
+
+ + {v.plateNumber} + + + {v.vehicleType} + +
+
+ + {(v.completionRate * 100).toFixed(1)}% + +
完成率
+
+
+ +
{v.targetName}
+ +
+
+
累计里程
+
{fmtKm(v.totalMileage)} km
+
+
+
年度目标
+
{fmtKm(v.yearTarget)} km
+
+
+
+ 区域 +
+
{v.region}
+
+
+
客户日均
+
{fmtKm(v.customerAvgDaily)} km
+
+
+ + {v.customer && ( +
+ 客户:{v.customer} +
+ )} +
+ + {/* Reason Card */} +
+ 建议原因 + {s.reason} +
+ + {/* Candidates Section */} +
+
+ + 推荐替换车辆 +
+
基于车型、区域及里程匹配
+ +
+ {s.candidates.map(c => { + const alreadySent = sentPlates.has(c.plateNumber); + const predColor = + c.canQualifyAfterSwap ? 'text-emerald-600' : 'text-amber-600'; + + return ( +
+
+
+ + {c.plateNumber} + + + {c.vehicleType} + + {c.targetName ? ( + {c.targetName} + ) : ( + 库存 + )} +
+ {c.canQualifyAfterSwap ? ( + + 换后可达标 + + ) : ( + + 需关注 + + )} +
+ +
+
+
当前里程
+
{fmtKm(c.totalMileage)} km
+
+
+
里程缺口
+
{fmtKm(c.mileageGap)} km
+
+
+
+ 区域 +
+
{c.region}
+
+
+
换后预测
+
{fmtKm(c.predictedAfterSwap)} km
+
+
+ + +
+ ); + })} +
+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}