You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
6.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"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>
);
}