"use client"; import { useCallback, useRef, useState } from "react"; import type { AddressInput, MothershipAddressCandidate, QuoteDetail, QuotePageStatus, QuoteRequestBody, } from "@/lib/frontend/types"; import { SUBMIT_LOCK_MS } from "@/lib/frontend/constants"; import { hostCreateQuote, hostFetchMothershipCandidates, hostGetQuote, hostPreheatQuoteSession, hostReleaseQuoteSession, } from "@/lib/frontend/api-client"; import { applyMothershipCandidate } from "@/lib/frontend/mothership-address"; import { pollQuoteUntilDone } from "@/hooks/use-quote-polling"; import { formatQuoteErrorMessage } from "@/modules/quote/quote-error-messages"; import { QuoteForm } from "@/components/quote/quote-form"; import { QuoteResultPanel } from "@/components/quote/quote-result-panel"; import { AddressDisambiguationModal } from "@/components/quote/address-disambiguation-modal"; import { PrimaryButton } from "@/components/ui/primary-button"; import { ErrorBanner } from "@/components/ui/error-banner"; import { QuoteProviderSwitch, type QuoteProviderId, } from "@/components/embed/quote-provider-switch"; import { Priority1QuoteWidget } from "@/components/priority1/priority1-quote-widget"; export interface EmbeddedQuoteWidgetProps { customerId: string; apiBaseUrl?: string; /** 未传时使用 Cookie 演示会话鉴权(/embed-demo) */ serviceToken?: string; } const FORM_ID = "embedded-quote-form"; export function EmbeddedQuoteWidget({ serviceToken, customerId, apiBaseUrl = "", }: EmbeddedQuoteWidgetProps) { const [provider, setProvider] = useState("mothership"); const [status, setStatus] = useState("idle"); const [quote, setQuote] = useState(null); const [error, setError] = useState(null); const [submitLocked, setSubmitLocked] = useState(false); const [disambiguationOpen, setDisambiguationOpen] = useState(false); const [confirmedPickup, setConfirmedPickup] = useState( null, ); const [confirmedDelivery, setConfirmedDelivery] = useState(null); const [pickupCandidates, setPickupCandidates] = useState< MothershipAddressCandidate[] >([]); const [deliveryCandidates, setDeliveryCandidates] = useState< MothershipAddressCandidate[] >([]); const pendingBodyRef = useRef(null); const pendingSessionRef = useRef(undefined); const reset = useCallback(() => { setStatus("idle"); setQuote(null); setError(null); setConfirmedPickup(null); setConfirmedDelivery(null); }, []); const finishQuote = useCallback((detail: QuoteDetail) => { setQuote(detail); if (detail.status === "failed") { setStatus("error"); setError( detail.error_message ?? formatQuoteErrorMessage(detail.error_code), ); return; } if (detail.status === "expired") { setStatus("expired"); return; } setStatus(detail.is_realtime === false ? "fallback" : "success"); setError(null); }, []); const submitQuote = useCallback( async (body: QuoteRequestBody) => { setStatus("validating"); setError(null); const created = await hostCreateQuote(apiBaseUrl, serviceToken, body); if (created.code !== 0) { setStatus("error"); setError(created.message); return; } const { quote_id, status: createStatus } = created.data; setStatus("processing"); if (createStatus === "done") { const detail = await hostGetQuote( apiBaseUrl, serviceToken, customerId, quote_id, ); if (detail.code !== 0) { setStatus("error"); setError(detail.message); return; } finishQuote(detail.data); return; } const pollResult = await pollQuoteUntilDone(async () => { try { const res = await hostGetQuote( apiBaseUrl, serviceToken, customerId, quote_id, ); if (res.code !== 0) { return { ok: false, errorMessage: res.message }; } return { ok: true, data: res.data }; } catch { return { ok: false }; } }); if (pollResult.type === "done") { finishQuote(pollResult.quote); return; } setStatus("error"); setError( pollResult.type === "timeout" ? formatQuoteErrorMessage("QUOTE_TIMEOUT") : pollResult.message, ); }, [apiBaseUrl, serviceToken, customerId, finishQuote], ); const handleSubmit = useCallback( async (body: QuoteRequestBody) => { if (submitLocked) return; setSubmitLocked(true); setTimeout(() => setSubmitLocked(false), SUBMIT_LOCK_MS); setStatus("resolving_address"); setError(null); let candidatesRes; try { candidatesRes = await hostFetchMothershipCandidates( apiBaseUrl, serviceToken, customerId, body.pickup_address, body.delivery_address, ); } catch { setStatus("error"); setError("地址联想请求失败,请稍后重试"); return; } if (candidatesRes.code !== 0) { setStatus("error"); setError(candidatesRes.message); return; } const { pickup_candidates, delivery_candidates, quote_session_id, } = candidatesRes.data; pendingSessionRef.current = quote_session_id; pendingBodyRef.current = body; setPickupCandidates(pickup_candidates); setDeliveryCandidates(delivery_candidates); setDisambiguationOpen(true); if (quote_session_id) { void hostPreheatQuoteSession( apiBaseUrl, serviceToken, customerId, quote_session_id, ); } setStatus("idle"); }, [apiBaseUrl, serviceToken, customerId, submitLocked], ); const handleDisambiguationConfirm = useCallback( async ( pickup: MothershipAddressCandidate, delivery: MothershipAddressCandidate, ) => { const base = pendingBodyRef.current; if (!base) { return; } setDisambiguationOpen(false); pendingBodyRef.current = null; const confirmedBody: QuoteRequestBody = { ...base, quote_session_id: pendingSessionRef.current, pickup_address: applyMothershipCandidate(base.pickup_address, pickup), delivery_address: applyMothershipCandidate( base.delivery_address, delivery, ), }; setConfirmedPickup(confirmedBody.pickup_address); setConfirmedDelivery(confirmedBody.delivery_address); pendingSessionRef.current = undefined; await submitQuote(confirmedBody); }, [submitQuote], ); const handleDisambiguationCancel = useCallback(() => { const sessionId = pendingSessionRef.current; setDisambiguationOpen(false); pendingBodyRef.current = null; pendingSessionRef.current = undefined; setStatus("idle"); if (sessionId) { void hostReleaseQuoteSession( apiBaseUrl, serviceToken, customerId, sessionId, ); } }, [apiBaseUrl, serviceToken, customerId]); const formDisabled = status === "resolving_address" || status === "validating" || status === "processing" || submitLocked || disambiguationOpen; const switchDisabled = status === "validating" || status === "processing" || status === "resolving_address"; return (

卡派查价

选择数据源填写货物信息,获取实时报价

{ setProvider(next); reset(); }} disabled={switchDisabled} /> {provider === "priority1" ? ( ) : ( <> {error && status === "idle" && (
{error}
)}
{ setConfirmedPickup(null); setConfirmedDelivery(null); }} onValidSubmit={(body) => void handleSubmit(body)} />
获取报价
setStatus("expired")} onExpiredConfirm={reset} />
void handleDisambiguationConfirm(pickup, delivery) } onCancel={handleDisambiguationCancel} /> )}
); }