feat(scheduling): add full-screen SwapPreview for screenshot sharing
New SwapPreview component replaces direct "发送通知" button: - Full-screen white background for clean screenshots - Swap diagram: current vehicle → arrow → replacement vehicle - Replacement reason section - Post-swap prediction: predicted mileage, target, conclusion - "发送替换通知" button at bottom - Candidate button in detail modal changed to "查看替换方案 →" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<CandidateVehicle | null>(null);
|
||||
const [sentPlates, setSentPlates] = useState<Set<string>>(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 (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[60] flex items-end sm:items-center justify-center" onClick={onClose}>
|
||||
<motion.div
|
||||
@@ -178,15 +156,15 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
|
||||
{/* Action */}
|
||||
<div className="px-3 pb-2.5">
|
||||
<button
|
||||
onClick={() => handleNotify(c)}
|
||||
disabled={sending || sent}
|
||||
onClick={() => setPreviewCandidate(c)}
|
||||
disabled={sent}
|
||||
className={`w-full flex items-center justify-center gap-1.5 text-[11px] font-bold py-2 rounded-lg transition-all cursor-pointer ${
|
||||
sent
|
||||
? 'bg-slate-100 text-slate-400'
|
||||
? 'bg-emerald-50 text-emerald-600'
|
||||
: 'bg-slate-800 hover:bg-slate-900 text-white active:scale-[0.98] shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{sent ? <><CheckCircle size={12} /> 已发送</> : <><Send size={12} /> 发送替换通知</>}
|
||||
{sent ? <><CheckCircle size={12} /> 已通知</> : <>查看替换方案 <ArrowRight size={12} /></>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,6 +184,20 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Full-screen swap preview */}
|
||||
{previewCandidate && (
|
||||
<SwapPreview
|
||||
suggestion={s}
|
||||
candidate={previewCandidate}
|
||||
onClose={() => setPreviewCandidate(null)}
|
||||
onSuccess={() => {
|
||||
setSentPlates(prev => new Set(prev).add(previewCandidate.plateNumber));
|
||||
setPreviewCandidate(null);
|
||||
onNotifySuccess();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user