|
|
"use client";
|
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
import { CheckCircle, Database, ClockClockwise } from "@phosphor-icons/react";
|
|
|
import type {
|
|
|
QuoteDetail,
|
|
|
QuoteItem,
|
|
|
RateOption,
|
|
|
ServiceLevel,
|
|
|
} from "@/lib/frontend/types";
|
|
|
import {
|
|
|
filterQuotesForMotherShipUiDisplay,
|
|
|
getMotherShipRateOptionLabel,
|
|
|
getMotherShipServiceLevelLabel,
|
|
|
listMotherShipUiTabsFromQuotes,
|
|
|
listVisibleRateOptionsForTab,
|
|
|
resolveDefaultRateOptionForTab,
|
|
|
} from "@/lib/constants/mothership-ui-tiers";
|
|
|
import { formatUSD, formatPercent } from "@/lib/frontend/format";
|
|
|
import { Card } from "@/components/ui/card";
|
|
|
|
|
|
const RATE_ORDER: RateOption[] = ["lowest", "bestValue", "fastest"];
|
|
|
|
|
|
interface QuoteCardProps {
|
|
|
quote: QuoteDetail;
|
|
|
defaultLevel?: ServiceLevel;
|
|
|
defaultRate?: RateOption;
|
|
|
}
|
|
|
|
|
|
function findTier(
|
|
|
quotes: QuoteItem[],
|
|
|
level: ServiceLevel,
|
|
|
rate: RateOption,
|
|
|
): QuoteItem | undefined {
|
|
|
return quotes.find(
|
|
|
(q) => q.service_level === level && q.rate_option === rate,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function SourceBadge({ quote }: { quote: QuoteDetail }) {
|
|
|
if (quote.source_type === "cache") {
|
|
|
return (
|
|
|
<span className="inline-flex items-center gap-1 rounded-sm bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
|
|
<Database size={12} weight="fill" />
|
|
|
缓存命中
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
if (quote.source_type === "stale") {
|
|
|
return (
|
|
|
<span className="inline-flex items-center gap-1 rounded-sm bg-amber-100 px-2 py-0.5 text-xs text-warning">
|
|
|
<ClockClockwise size={12} weight="fill" />
|
|
|
非实时
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
return (
|
|
|
<span className="inline-flex items-center gap-1 rounded-sm bg-green-100 px-2 py-0.5 text-xs text-success">
|
|
|
<CheckCircle size={12} weight="fill" />
|
|
|
实时报价
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export function QuoteCard({
|
|
|
quote,
|
|
|
defaultLevel = "standard",
|
|
|
defaultRate = "lowest",
|
|
|
}: QuoteCardProps) {
|
|
|
const displayQuotes = useMemo(
|
|
|
() => filterQuotesForMotherShipUiDisplay(quote.quotes ?? []),
|
|
|
[quote.quotes],
|
|
|
);
|
|
|
const serviceLevels = useMemo(
|
|
|
() => listMotherShipUiTabsFromQuotes(displayQuotes),
|
|
|
[displayQuotes],
|
|
|
);
|
|
|
|
|
|
const [level, setLevel] = useState<ServiceLevel>(defaultLevel);
|
|
|
const [rate, setRate] = useState<RateOption>(defaultRate);
|
|
|
|
|
|
const availableRates = useMemo(
|
|
|
() => listVisibleRateOptionsForTab(displayQuotes, level) as RateOption[],
|
|
|
[displayQuotes, level],
|
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (serviceLevels.length > 0 && !serviceLevels.includes(level as string)) {
|
|
|
setLevel(serviceLevels[0] as ServiceLevel);
|
|
|
}
|
|
|
}, [serviceLevels, level]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
const preferred = resolveDefaultRateOptionForTab(displayQuotes, level);
|
|
|
if (availableRates.length > 0 && !availableRates.includes(rate)) {
|
|
|
setRate(preferred as RateOption);
|
|
|
}
|
|
|
}, [availableRates, rate, displayQuotes, level]);
|
|
|
|
|
|
const item =
|
|
|
findTier(displayQuotes, level, rate) ??
|
|
|
findTier(
|
|
|
displayQuotes,
|
|
|
level,
|
|
|
resolveDefaultRateOptionForTab(displayQuotes, level) as RateOption,
|
|
|
) ??
|
|
|
displayQuotes.find((q) => q.service_level === level);
|
|
|
|
|
|
if (!item) {
|
|
|
return (
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">暂无报价数据</p>
|
|
|
</Card>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
<Card className="relative">
|
|
|
{!quote.is_realtime && (
|
|
|
<span className="absolute right-4 top-4 rounded-sm bg-amber-100 px-2 py-0.5 text-xs text-warning">
|
|
|
非实时报价,仅供参考
|
|
|
</span>
|
|
|
)}
|
|
|
|
|
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
|
|
<div className="flex gap-1 rounded-md bg-bg p-1">
|
|
|
{serviceLevels.map((lv) => (
|
|
|
<button
|
|
|
key={lv}
|
|
|
type="button"
|
|
|
onClick={() => setLevel(lv as ServiceLevel)}
|
|
|
className={`rounded-sm px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
|
level === lv
|
|
|
? "bg-surface text-primary shadow-card"
|
|
|
: "text-text-secondary"
|
|
|
}`}
|
|
|
>
|
|
|
{getMotherShipServiceLevelLabel(lv)}
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
<SourceBadge quote={quote} />
|
|
|
</div>
|
|
|
|
|
|
<div className="mb-3 flex gap-1 rounded-md bg-bg p-1">
|
|
|
{(availableRates.length > 0 ? availableRates : RATE_ORDER).map((rt) => {
|
|
|
if (!displayQuotes.some(
|
|
|
(q) => q.service_level === level && q.rate_option === rt,
|
|
|
)) {
|
|
|
return null;
|
|
|
}
|
|
|
return (
|
|
|
<button
|
|
|
key={rt}
|
|
|
type="button"
|
|
|
onClick={() => setRate(rt)}
|
|
|
className={`rounded-sm px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
|
rate === rt
|
|
|
? "bg-surface text-primary shadow-card"
|
|
|
: "text-text-secondary"
|
|
|
}`}
|
|
|
>
|
|
|
{getMotherShipRateOptionLabel(level, rt)}
|
|
|
</button>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-end justify-between gap-4">
|
|
|
<div>
|
|
|
<p className="text-sm text-text-secondary">
|
|
|
{item.carrier} · {item.transit_description}
|
|
|
</p>
|
|
|
<p className="mt-1 font-mono text-3xl font-semibold text-text-primary num">
|
|
|
{formatUSD(item.final_total)}
|
|
|
</p>
|
|
|
</div>
|
|
|
<div className="text-right text-sm text-text-secondary">
|
|
|
<p>时效 {item.transit_days} 天</p>
|
|
|
<p className="num">原价 {formatUSD(item.raw_total)}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-5 border-t border-border pt-4">
|
|
|
<p className="mb-2 text-sm font-medium text-text-secondary">价格明细</p>
|
|
|
<ul className="space-y-1.5 text-sm">
|
|
|
{item.breakdown.map((b) => (
|
|
|
<li key={b.item} className="flex justify-between">
|
|
|
<span className="text-text-secondary">{b.item}</span>
|
|
|
<span className="num font-mono text-text-primary">
|
|
|
{formatUSD(b.amount)}
|
|
|
</span>
|
|
|
</li>
|
|
|
))}
|
|
|
<li className="flex justify-between border-t border-border pt-1.5 font-medium">
|
|
|
<span>客户最终价(加价 {formatPercent(item.markup_percent)})</span>
|
|
|
<span className="num font-mono text-primary">
|
|
|
{formatUSD(item.final_total)}
|
|
|
</span>
|
|
|
</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-3 font-mono text-xs text-text-disabled">
|
|
|
单号 {quote.quote_id}
|
|
|
</p>
|
|
|
</Card>
|
|
|
);
|
|
|
}
|