|
|
/**
|
|
|
* Flock 查价可视化进度(以 Worker 回写的 rpa_stage 为准,失败时标出卡点)
|
|
|
*/
|
|
|
"use client";
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
|
|
import {
|
|
|
FLOCK_RPA_STAGE_LABELS,
|
|
|
FLOCK_RPA_STAGES,
|
|
|
flockStageIndex,
|
|
|
mapFlockErrorToStage,
|
|
|
type FlockRpaStage,
|
|
|
} from "@/lib/flock/rpa-progress";
|
|
|
|
|
|
const DISPLAY_STAGES: FlockRpaStage[] = [
|
|
|
"queued",
|
|
|
"lock",
|
|
|
"login",
|
|
|
"open_quote",
|
|
|
"fill_form",
|
|
|
"submit",
|
|
|
"wait_quote",
|
|
|
];
|
|
|
|
|
|
export interface FlockQuoteProgressProps {
|
|
|
active: boolean;
|
|
|
quoteId?: string | null;
|
|
|
startedAtMs?: number | null;
|
|
|
/** Worker 回写阶段 */
|
|
|
rpaStage?: string | null;
|
|
|
rpaStageLabel?: string | null;
|
|
|
/** 失败后的错误原文(用于把进度钉在卡点并展示 hint) */
|
|
|
errorMessage?: string | null;
|
|
|
failed?: boolean;
|
|
|
}
|
|
|
|
|
|
export function FlockQuoteProgress({
|
|
|
active,
|
|
|
quoteId,
|
|
|
startedAtMs,
|
|
|
rpaStage,
|
|
|
rpaStageLabel,
|
|
|
errorMessage,
|
|
|
failed = false,
|
|
|
}: FlockQuoteProgressProps) {
|
|
|
const [now, setNow] = useState(() => Date.now());
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!active && !failed) return;
|
|
|
const t = window.setInterval(() => setNow(Date.now()), 500);
|
|
|
return () => window.clearInterval(t);
|
|
|
}, [active, failed]);
|
|
|
|
|
|
if (!active && !failed) return null;
|
|
|
|
|
|
const mapped = failed ? mapFlockErrorToStage(errorMessage) : null;
|
|
|
// 优先 Worker 回写的 rpa_stage;禁止失败时用默认「互斥锁」覆盖真实进度
|
|
|
const stageRaw = (
|
|
|
rpaStage && (FLOCK_RPA_STAGES as readonly string[]).includes(rpaStage)
|
|
|
? rpaStage
|
|
|
: (mapped?.stage ?? "queued")
|
|
|
) as string;
|
|
|
const currentStage: FlockRpaStage = (
|
|
|
FLOCK_RPA_STAGES as readonly string[]
|
|
|
).includes(stageRaw)
|
|
|
? (stageRaw as FlockRpaStage)
|
|
|
: "queued";
|
|
|
const currentIdx = Math.max(0, flockStageIndex(currentStage));
|
|
|
const label =
|
|
|
(failed ? mapped?.hint : null) ??
|
|
|
rpaStageLabel ??
|
|
|
FLOCK_RPA_STAGE_LABELS[currentStage];
|
|
|
const elapsed = Math.max(0, now - (startedAtMs ?? now));
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
className={[
|
|
|
"rounded-lg border p-4 shadow-card",
|
|
|
failed
|
|
|
? "border-red-300 bg-red-50"
|
|
|
: "border-[#1890FF]/25 bg-[#1890FF]/5",
|
|
|
].join(" ")}
|
|
|
role="status"
|
|
|
aria-live="polite"
|
|
|
data-testid="flock-quote-progress"
|
|
|
data-rpa-stage={currentStage}
|
|
|
>
|
|
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
|
<div
|
|
|
className={[
|
|
|
"flex items-center gap-2 text-sm font-semibold",
|
|
|
failed ? "text-red-700" : "text-[#1F2937]",
|
|
|
].join(" ")}
|
|
|
>
|
|
|
{!failed ? (
|
|
|
<LoadingSpinner size={16} className="text-[#1890FF]" />
|
|
|
) : null}
|
|
|
{failed ? "Flock 查价失败 — 已定位卡点" : "正在查询 Flock Freight 实时报价"}
|
|
|
</div>
|
|
|
<span className="text-xs tabular-nums text-text-secondary">
|
|
|
已用时 {(elapsed / 1000).toFixed(1)}s
|
|
|
</span>
|
|
|
</div>
|
|
|
|
|
|
<ol className="space-y-2">
|
|
|
{DISPLAY_STAGES.map((stage) => {
|
|
|
const i = flockStageIndex(stage);
|
|
|
const done = !failed && i < currentIdx;
|
|
|
const current = i === currentIdx || (failed && stage === currentStage);
|
|
|
const stuck = failed && stage === currentStage;
|
|
|
return (
|
|
|
<li
|
|
|
key={stage}
|
|
|
className={[
|
|
|
"flex items-start gap-2 text-sm",
|
|
|
stuck
|
|
|
? "font-semibold text-red-600"
|
|
|
: current
|
|
|
? "font-medium text-[#1890FF]"
|
|
|
: done
|
|
|
? "text-[#1F2937]"
|
|
|
: "text-text-disabled",
|
|
|
].join(" ")}
|
|
|
>
|
|
|
<span aria-hidden className="mt-0.5 w-4 shrink-0 text-center">
|
|
|
{stuck ? "✕" : done ? "✓" : current ? "●" : "○"}
|
|
|
</span>
|
|
|
<span>
|
|
|
{FLOCK_RPA_STAGE_LABELS[stage]}
|
|
|
{current && !failed ? "…" : null}
|
|
|
{stuck ? "(卡在这里)" : null}
|
|
|
</span>
|
|
|
</li>
|
|
|
);
|
|
|
})}
|
|
|
</ol>
|
|
|
|
|
|
<p
|
|
|
className={[
|
|
|
"mt-3 text-xs",
|
|
|
failed ? "font-medium text-red-700" : "text-text-secondary",
|
|
|
].join(" ")}
|
|
|
>
|
|
|
{label}
|
|
|
</p>
|
|
|
|
|
|
{quoteId ? (
|
|
|
<p className="mt-2 break-all text-[11px] text-text-secondary">
|
|
|
询价单号:{quoteId}
|
|
|
</p>
|
|
|
) : null}
|
|
|
{!failed ? (
|
|
|
<p className="mt-1 text-[11px] text-text-disabled">
|
|
|
进度由 RPA Worker 实时回写;若长时间停在「登录」,请核对客户 Flock
|
|
|
账密、JWT_SECRET,或有头模式查看 .rpa/flock-stalls/ 截图。
|
|
|
</p>
|
|
|
) : null}
|
|
|
</div>
|
|
|
);
|
|
|
}
|