|
|
"use client";
|
|
|
|
|
|
import { useCallback, 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 { 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 } 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";
|
|
|
|
|
|
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 [status, setStatus] = useState<QuotePageStatus>("idle");
|
|
|
const [quote, setQuote] = useState<QuoteDetail | null>(null);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [submitLocked, setSubmitLocked] = useState(false);
|
|
|
const [activeQuoteId, setActiveQuoteId] = useState<string | null>(null);
|
|
|
const [startedAtMs, setStartedAtMs] = useState<number | null>(null);
|
|
|
const [rpaStage, setRpaStage] = useState<string | null>(null);
|
|
|
const [rpaStageLabel, setRpaStageLabel] = useState<string | null>(null);
|
|
|
|
|
|
const reset = useCallback(() => {
|
|
|
setStatus("idle");
|
|
|
setQuote(null);
|
|
|
setError(null);
|
|
|
setActiveQuoteId(null);
|
|
|
setStartedAtMs(null);
|
|
|
setRpaStage(null);
|
|
|
setRpaStageLabel(null);
|
|
|
}, []);
|
|
|
|
|
|
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);
|
|
|
if (detail.status === "failed") {
|
|
|
setStatus("error");
|
|
|
setError(
|
|
|
detail.error_message ??
|
|
|
formatQuoteErrorMessage(detail.error_code) ??
|
|
|
"报价失败",
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
if (detail.status === "expired") {
|
|
|
setStatus("expired");
|
|
|
return;
|
|
|
}
|
|
|
if (!detail.flock?.lines?.length) {
|
|
|
setStatus("error");
|
|
|
setError("未返回 Flock 报价档位");
|
|
|
return;
|
|
|
}
|
|
|
setStatus(detail.is_realtime === false ? "fallback" : "success");
|
|
|
setError(null);
|
|
|
}, []);
|
|
|
|
|
|
const handleSubmit = useCallback(
|
|
|
async (payload: FlockFormSubmitPayload & Record<string, unknown>) => {
|
|
|
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");
|
|
|
|
|
|
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],
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-4">
|
|
|
{error && status === "error" && <ErrorBanner>{error}</ErrorBanner>}
|
|
|
{status === "expired" && (
|
|
|
<WarningBanner>报价已过期,请重新查询</WarningBanner>
|
|
|
)}
|
|
|
|
|
|
<FlockQuoteProgress
|
|
|
active={status === "processing"}
|
|
|
failed={status === "error"}
|
|
|
quoteId={activeQuoteId}
|
|
|
startedAtMs={startedAtMs}
|
|
|
rpaStage={rpaStage}
|
|
|
rpaStageLabel={rpaStageLabel}
|
|
|
errorMessage={error}
|
|
|
/>
|
|
|
|
|
|
{status === "success" || status === "fallback" ? (
|
|
|
quote?.flock ? (
|
|
|
<FlockQuoteResult
|
|
|
lines={quote.flock.lines}
|
|
|
reference={quote.flock.reference}
|
|
|
onReset={reset}
|
|
|
/>
|
|
|
) : null
|
|
|
) : flockLoggedInUi ? (
|
|
|
<FlockLoggedInQuoteForm
|
|
|
disabled={status === "processing"}
|
|
|
submitLocked={submitLocked || status === "processing"}
|
|
|
customerId={customerId}
|
|
|
onValidSubmit={handleLoggedInSubmit}
|
|
|
/>
|
|
|
) : (
|
|
|
<FlockQuoteForm
|
|
|
disabled={status === "processing"}
|
|
|
submitLocked={submitLocked || status === "processing"}
|
|
|
onSubmit={handleSubmit}
|
|
|
/>
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
}
|