"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { FlockQuoteForm, type FlockFormSubmitPayload, } from "@/components/flock/flock-quote-form"; import { FlockLoggedInQuoteForm, mapFlockLoggedInToApiInput, type FlockLoggedInQuotePayload, } from "@/components/flock/flock-logged-in-quote-form"; import { FlockLoggedInDetailsForm, type FlockLoggedInDetailsFormHandle, } from "@/components/flock/flock-logged-in-details-form"; import { FlockLoggedInQuoteSidebar, } from "@/components/flock/flock-logged-in-quote-sidebar"; import { FlockQuoteProgress } from "@/components/flock/flock-quote-progress"; import { FlockQuoteResult } from "@/components/flock/flock-quote-result"; import { WarningBanner } from "@/components/ui/warning-banner"; import { ErrorBanner } from "@/components/ui/error-banner"; import { SUBMIT_LOCK_MS } from "@/lib/frontend/constants"; import { hostCreateFlockQuote, hostGetQuote, hostSubmitFlockCheckout, hostSubmitFlockPricingOptions, hostContinueFlockQuoteHold, hostDeclineFlockQuoteHold, } from "@/lib/frontend/api-client"; import { uuidV4 } from "@/lib/frontend/format"; import type { QuoteDetail, QuotePageStatus } from "@/lib/frontend/types"; import { pollQuoteUntilDone } from "@/hooks/use-quote-polling"; import { getFlockPollTimeoutMs } from "@/lib/flock/poll-timeout"; import { formatQuoteErrorMessage } from "@/modules/quote/quote-error-messages"; import { useHostBridgeOptional } from "@/lib/embed/host-bridge-react"; import type { FlockCarrierOption, FlockCheckoutTier, FlockFlexibilityKey, FlockFlexibilityOption, } from "@/lib/flock/flock-checkout-selection"; import { FlockQuoteHoldPrompt } from "@/components/flock/flock-quote-hold-prompt"; export interface FlockQuoteWidgetProps { serviceToken?: string; customerId: string; apiBaseUrl?: string; /** 客户已绑定 Flock 账密 → 显示登录后 Quick 查价界面 */ flockLoggedInUi?: boolean; } /** Flock Freight 内嵌查价:表单 + 可视化进度 + 轮询 + 两档结果 */ export function FlockQuoteWidget({ serviceToken, customerId, apiBaseUrl = "", flockLoggedInUi = false, }: FlockQuoteWidgetProps) { const hostBridge = useHostBridgeOptional(); const [status, setStatus] = useState("idle"); const [quote, setQuote] = useState(null); const [error, setError] = useState(null); const [submitLocked, setSubmitLocked] = useState(false); const [activeQuoteId, setActiveQuoteId] = useState(null); const [startedAtMs, setStartedAtMs] = useState(null); const [rpaStage, setRpaStage] = useState(null); const [rpaStageLabel, setRpaStageLabel] = useState(null); const [checkoutBusy, setCheckoutBusy] = useState(false); const [checkoutMessage, setCheckoutMessage] = useState(null); const [selectedTier, setSelectedTier] = useState( null, ); const [selectedFlexibility, setSelectedFlexibility] = useState(null); const [selectedCarrier, setSelectedCarrier] = useState(null); /** 兜底二次拉价时写入对应档(首价同步优先走 quote.flock_flexibility_by_tier) */ const [flexFallbackByTier, setFlexFallbackByTier] = useState< Partial> >({}); const [carrierFallbackByTier, setCarrierFallbackByTier] = useState< Partial> >({}); const [flexBusy, setFlexBusy] = useState(false); const [flexMessage, setFlexMessage] = useState(null); const [flockHold, setFlockHold] = useState< NonNullable | null >(null); const [holdAccepted, setHoldAccepted] = useState(false); const checkoutAbortRef = useRef(null); const flexAbortRef = useRef(null); const detailsRef = useRef(null); const reset = useCallback(() => { setStatus("idle"); setQuote(null); setError(null); setActiveQuoteId(null); setStartedAtMs(null); setRpaStage(null); setRpaStageLabel(null); setCheckoutBusy(false); setCheckoutMessage(null); setSelectedTier(null); setSelectedFlexibility(null); setSelectedCarrier(null); setFlexFallbackByTier({}); setCarrierFallbackByTier({}); setFlexBusy(false); setFlexMessage(null); setFlockHold(null); setHoldAccepted(false); checkoutAbortRef.current?.abort(); flexAbortRef.current?.abort(); }, []); const finishQuote = useCallback( (detail: QuoteDetail) => { setQuote(detail); setActiveQuoteId(detail.quote_id ?? null); if (detail.rpa_stage) setRpaStage(detail.rpa_stage); if (detail.rpa_stage_label) setRpaStageLabel(detail.rpa_stage_label); const module = flockLoggedInUi ? "FLOCK_LOGGED_IN" : "FLOCK_GUEST"; const mapQuotes = () => (detail.quotes ?? []).map((q) => ({ carrier: q.carrier, service_level: q.service_level, rate_option: q.rate_option, transit_days: q.transit_days, transit_description: q.transit_description, raw_freight: q.raw_freight, surcharges: q.surcharges, raw_total: q.raw_total, markup_percent: q.markup_percent, markup_amount: q.markup_amount, final_total: q.final_total, breakdown: q.breakdown ?? [], })); if (detail.status === "failed") { setStatus("error"); const msg = detail.error_message ?? formatQuoteErrorMessage(detail.error_code) ?? "报价失败"; setError(msg); hostBridge?.reportQuoteResult({ quote_id: detail.quote_id, request_id: detail.request_id, status: "failed", module, currency: detail.currency || "USD", quotes: [], error_code: detail.error_code, error_message: msg, }); return; } if (detail.status === "expired") { setStatus("expired"); hostBridge?.reportQuoteResult({ quote_id: detail.quote_id, request_id: detail.request_id, status: "expired", module, currency: detail.currency || "USD", quotes: [], error_code: detail.error_code, error_message: detail.error_message, }); return; } if (!detail.flock?.lines?.length && !(detail.quotes?.length)) { setStatus("error"); setError("未返回 Flock 报价档位"); hostBridge?.reportError("RPA_DATA_INVALID", "未返回 Flock 报价档位", module); return; } setStatus(detail.is_realtime === false ? "fallback" : "success"); setError(null); setSelectedTier(null); setSelectedFlexibility(null); setSelectedCarrier(null); setFlexFallbackByTier({}); setCarrierFallbackByTier({}); // 无保活会话时直接解锁二级(冷启动兜底) if (flockLoggedInUi && detail.flock_hold?.available && !holdAccepted) { setFlockHold(detail.flock_hold); setHoldAccepted(false); } else { setFlockHold(null); if (flockLoggedInUi && !detail.flock_hold?.available) { setHoldAccepted(true); } } hostBridge?.reportQuoteResult({ quote_id: detail.quote_id, request_id: detail.request_id, status: "done", module, currency: detail.currency || "USD", source_type: detail.source_type, is_realtime: detail.is_realtime, quotes: mapQuotes().length ? mapQuotes() : (detail.flock?.lines ?? []).map((line) => ({ carrier: line.carrier || "Flock Freight", service_level: line.serviceLevel || "standard", rate_option: line.rateOption || "lowest", transit_days: String(line.transitDays ?? ""), transit_description: line.transitDescription ?? line.label ?? "", raw_freight: Number(line.totalUsd ?? 0), surcharges: 0, raw_total: Number(line.totalUsd ?? 0), markup_percent: 0, markup_amount: Number(line.markup_amount ?? 0), final_total: Number(line.final_total_usd ?? line.totalUsd ?? 0), breakdown: [], })), error_code: null, error_message: null, }); }, [flockLoggedInUi, hostBridge, holdAccepted], ); const handleSubmit = useCallback( async (payload: FlockFormSubmitPayload & Record) => { if (submitLocked) return; setSubmitLocked(true); setTimeout(() => setSubmitLocked(false), SUBMIT_LOCK_MS); setError(null); setQuote(null); setActiveQuoteId(null); setStartedAtMs(Date.now()); setRpaStage("queued"); setRpaStageLabel("任务已入队,等待 Worker 领取"); setStatus("processing"); setCheckoutMessage(null); setHoldAccepted(false); setFlockHold(null); const created = await hostCreateFlockQuote(apiBaseUrl, serviceToken, { request_id: uuidV4(), customer_id: customerId, flock_input: payload, }); if (created.code !== 0) { setStatus("error"); setError(created.message || "提交失败"); return; } const { quote_id } = created.data; setActiveQuoteId(quote_id); 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 }; } }, { timeoutMs: getFlockPollTimeoutMs(), onProcessing: (detail) => { if (detail.rpa_stage) setRpaStage(detail.rpa_stage); if (detail.rpa_stage_label) setRpaStageLabel(detail.rpa_stage_label); }, }, ); if (pollResult.type === "done") { finishQuote(pollResult.quote); return; } setStatus("error"); if (pollResult.type === "error" && pollResult.quote?.rpa_stage) { setRpaStage(pollResult.quote.rpa_stage); setRpaStageLabel(pollResult.quote.rpa_stage_label ?? null); } setError( pollResult.type === "timeout" ? "Flock 询价超时,请稍后重试(可在进度条确认停在哪一步)" : pollResult.message || "查询失败", ); }, [apiBaseUrl, customerId, finishQuote, serviceToken, submitLocked], ); const handleLoggedInSubmit = useCallback( (payload: FlockLoggedInQuotePayload) => { const mapped = mapFlockLoggedInToApiInput(payload); void handleSubmit(mapped); }, [handleSubmit], ); const handleSelectTier = useCallback( async (tier: FlockCheckoutTier) => { if (!quote?.quote_id) return; setSelectedTier(tier); setSelectedFlexibility(null); setSelectedCarrier(null); flexAbortRef.current?.abort(); const cachedFlex = quote.flock_flexibility_by_tier?.[tier] ?? flexFallbackByTier[tier]; const cachedCarriers = quote.flock_carriers_by_tier?.[tier] ?? carrierFallbackByTier[tier]; if ( (cachedFlex && cachedFlex.length > 0) || (cachedCarriers && cachedCarriers.length > 0) ) { setFlexBusy(false); setFlexMessage(null); return; } // 兜底:首价未带回该档报价时二次拉价 const ac = new AbortController(); flexAbortRef.current = ac; setFlexBusy(true); setFlexMessage("正在官网拉取档内报价…"); try { const res = await hostSubmitFlockPricingOptions( apiBaseUrl, serviceToken, customerId, { quoteId: quote.quote_id, preferredTier: tier }, ); if (ac.signal.aborted) return; if (res.code !== 0) { setFlexMessage(res.message); setFlexBusy(false); return; } const deadline = Date.now() + 6 * 60_000; while (Date.now() < deadline) { if (ac.signal.aborted) return; await new Promise((resolve) => setTimeout(resolve, 2_000)); if (ac.signal.aborted) return; const r = await hostGetQuote( apiBaseUrl, serviceToken, customerId, quote.quote_id, ); if (ac.signal.aborted) return; if (r.code !== 0) continue; const st = r.data.flock_pricing_options; if (!st || st.preferred_tier !== tier) continue; if (st.status === "done") { if ( st.options_type === "carriers" && (st.carrier_options?.length ?? 0) > 0 ) { setCarrierFallbackByTier((prev) => ({ ...prev, [tier]: st.carrier_options!, })); setFlexMessage(null); setFlexBusy(false); return; } if (st.options.length > 0) { setFlexFallbackByTier((prev) => ({ ...prev, [tier]: st.options, })); setFlexMessage(null); setFlexBusy(false); return; } } if (st.status === "failed") { setFlexMessage(st.message || "拉取档内报价失败"); setFlexBusy(false); return; } setFlexMessage(st.message || "正在官网拉取档内报价…"); } if (!ac.signal.aborted) { setFlexMessage("拉取档内报价超时,请重试"); setFlexBusy(false); } } catch (err) { if (ac.signal.aborted) return; setFlexMessage(err instanceof Error ? err.message : "拉取失败"); setFlexBusy(false); } }, [ quote, apiBaseUrl, serviceToken, customerId, flexFallbackByTier, carrierFallbackByTier, ], ); const showL2DetailsForm = holdAccepted && (!!selectedFlexibility || !!selectedCarrier); const acceptHoldIfNeeded = useCallback(() => { if (flockHold && !holdAccepted) { const sessionId = flockHold.quote_session_id; const qid = quote?.quote_id; setHoldAccepted(true); setFlockHold(null); if (qid && sessionId) { void hostContinueFlockQuoteHold( apiBaseUrl, serviceToken, customerId, qid, sessionId, ); } } }, [ flockHold, holdAccepted, quote?.quote_id, apiBaseUrl, serviceToken, customerId, ]); // 选中灵活价后若仍卡在 hold 决策窗,自动视为继续填写(避免无「继续填写」时永远不出二级) const handleSelectFlexibility = useCallback( (key: FlockFlexibilityKey) => { setSelectedFlexibility(key); setSelectedCarrier(null); acceptHoldIfNeeded(); }, [acceptHoldIfNeeded], ); const handleSelectCarrier = useCallback( (carrierName: string) => { setSelectedCarrier(carrierName); setSelectedFlexibility(null); acceptHoldIfNeeded(); }, [acceptHoldIfNeeded], ); const handleCheckout = useCallback(async () => { if (!quote?.quote_id) { setCheckoutMessage("请先完成询价"); return; } if (!selectedTier || (!selectedFlexibility && !selectedCarrier)) { setCheckoutMessage("请先选择服务档与报价选项"); return; } let flockDetails: | ReturnType | undefined; if (flockLoggedInUi) { const details = detailsRef.current?.validateAndGetDetails() ?? null; if (!details) { setCheckoutMessage("请先完善左侧结账详情"); return; } flockDetails = details; } checkoutAbortRef.current?.abort(); const ac = new AbortController(); checkoutAbortRef.current = ac; setCheckoutBusy(true); setCheckoutMessage("正在同步官网选择报价档…"); try { const res = await hostSubmitFlockCheckout( apiBaseUrl, serviceToken, customerId, { quoteId: quote.quote_id, preferredTier: selectedTier, preferredFlexibility: selectedFlexibility ?? undefined, preferredCarrier: selectedCarrier ?? undefined, flockDetails: flockDetails ?? undefined, }, ); if (ac.signal.aborted) return; if (res.code !== 0) { setCheckoutMessage(res.message); return; } const deadline = Date.now() + 8 * 60_000; while (Date.now() < deadline) { if (ac.signal.aborted) return; await new Promise((resolve, reject) => { const t = setTimeout(resolve, 2_000); ac.signal.addEventListener( "abort", () => { clearTimeout(t); reject(new DOMException("Aborted", "AbortError")); }, { once: true }, ); }).catch(() => undefined); if (ac.signal.aborted) return; const r = await hostGetQuote( apiBaseUrl, serviceToken, customerId, quote.quote_id, ); if (ac.signal.aborted) return; if (r.code !== 0) continue; const st = r.data.flock_checkout; if (!st) continue; if (st.status === "done") { setQuote(r.data); setCheckoutMessage( st.message || `已填齐详情(${selectedTier === "flock_direct" ? "FlockDirect®" : "Standard"},未支付)`, ); return; } if (st.status === "failed") { setCheckoutMessage(st.message || "结账同步失败"); return; } setCheckoutMessage(st.message || "正在同步官网…"); } if (!ac.signal.aborted) { setCheckoutMessage("结账同步超时,请稍后重试"); } } catch (err) { if (ac.signal.aborted) return; setCheckoutMessage( err instanceof Error ? err.message : "结账同步失败", ); } finally { if (checkoutAbortRef.current === ac) { setCheckoutBusy(false); } } }, [ quote, customerId, apiBaseUrl, serviceToken, flockLoggedInUi, selectedTier, selectedFlexibility, selectedCarrier, ]); useEffect(() => { return () => { checkoutAbortRef.current?.abort(); flexAbortRef.current?.abort(); }; }, []); const showLoggedInDetails = flockLoggedInUi && (status === "success" || status === "fallback") && !!quote?.flock; return (
{error && status === "error" && {error}} {status === "expired" && ( 报价已过期,请重新查询 )} {showLoggedInDetails ? (
{showL2DetailsForm ? ( ) : (

{!holdAccepted ? "请先确认是否继续填写二级信息" : "请先选择报价选项"}

  1. 右侧按官网展示 FlockDirect® / 标准 LTL 双卡
  2. 在 60 秒确认窗选择「继续填写」
  3. 点击「查看报价选项」,在弹窗中选择灵活价或承运商
  4. 完善左侧结账详情后前往结账(同会话同步,不支付)
)} void handleSelectTier(t)} onSelectFlexibility={handleSelectFlexibility} onSelectCarrier={handleSelectCarrier} onCheckout={() => void handleCheckout()} onReset={reset} />
) : status === "success" || status === "fallback" ? ( quote?.flock ? ( ) : null ) : flockLoggedInUi ? ( ) : ( )} {flockLoggedInUi && flockHold && quote?.quote_id ? ( { setHoldAccepted(true); setFlockHold(null); void hostContinueFlockQuoteHold( apiBaseUrl, serviceToken, customerId, quote.quote_id, flockHold.quote_session_id, ); }} onDecline={() => { setFlockHold(null); void hostDeclineFlockQuoteHold( apiBaseUrl, serviceToken, customerId, quote.quote_id, flockHold.quote_session_id, ); }} /> ) : null}
); }