Files
lotteryFront/src/features/hall/hall-bet-result-dialog.tsx
kang 5cbabb5d0f
Some checks failed
lotteryfront CI / build (push) Has been cancelled
feat(api): 按开注商支持获取赔率数据并完善相关功能
- 在getPlayEffective接口中增加provider_code参数支持
- 优化接口调用时对currency和provider_code的传递逻辑
- hall-bet-preview-dialog新增previewLineKey方法,解决列表渲染key冲突问题
- 修改投注和结果弹窗中开注商标题的国际化调用方式
- hall-betting-grid中增加选中开注商的默认逻辑,接口调用时传递provider_code
- 优化provider名称显示逻辑,完善开注商选择及摘要文本显示
- 订单详情页中显示开注商名称,提升显示准确性
- 补充i18n中开注商相关文案,支持多语言环境
- 更新PlayEffectiveOddsSlice和PlayEffectivePayload类型,加入provider_code字段支持
2026-07-09 10:51:45 +08:00

326 lines
15 KiB
TypeScript

"use client";
import Link from "next/link";
import { AlertTriangle, CheckCircle2, ClipboardList, Ticket, XCircle, XIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { formatMinorAsCurrency } from "@/lib/money";
import { playLabel } from "@/lib/play-labels";
import { cn } from "@/lib/utils";
import type { TicketPlaceData } from "@/types/api/ticket";
type HallBetResultDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
currencyCode: string;
data: TicketPlaceData | null;
jackpotEnabled?: boolean;
creditMode?: boolean;
};
const SUCCESS_ITEM_STATUSES = new Set(["pending_draw", "placed"]);
const FAILURE_ITEM_STATUSES = new Set(["failed", "refunded"]);
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
return providerName || providerCode || "-";
}
export function HallBetResultDialog({
open,
onOpenChange,
currencyCode,
data,
jackpotEnabled = false,
creditMode = false,
}: HallBetResultDialogProps) {
const { t } = useTranslation("player");
const successItems =
data?.items.filter(
(item) => item.status != null && SUCCESS_ITEM_STATUSES.has(item.status),
) ?? [];
const failedItems =
data?.items.filter(
(item) => item.status != null && FAILURE_ITEM_STATUSES.has(item.status),
) ?? [];
const pendingItems =
data?.items.filter((item) => item.status === "pending_confirm") ?? [];
const orderStatus = data?.summary.order_status;
const isRefundedOrder = orderStatus === "refunded";
const totalSuccess = data?.summary.success_count ?? successItems.length;
const totalFailure = data?.summary.failure_count ?? failedItems.length;
const pendingConfirmCount =
data?.summary.pending_confirm_count ?? pendingItems.length;
const isAllFailed =
isRefundedOrder ||
(totalFailure > 0 && totalSuccess === 0 && pendingConfirmCount === 0);
const isPartial =
!isRefundedOrder &&
totalSuccess > 0 &&
(totalFailure > 0 || pendingConfirmCount > 0);
const isPendingOnly =
!isRefundedOrder &&
pendingConfirmCount > 0 &&
totalSuccess === 0 &&
totalFailure === 0;
const ResultIcon = isAllFailed
? XCircle
: isPartial || isPendingOnly
? AlertTriangle
: CheckCircle2;
const iconWrapClass = isAllFailed
? "border-rose-100 text-rose-600 shadow-[0_10px_24px_rgba(225,29,72,0.12)]"
: isPartial || isPendingOnly
? "border-amber-100 text-amber-600 shadow-[0_10px_24px_rgba(217,119,6,0.12)]"
: "border-emerald-100 text-emerald-600 shadow-[0_10px_24px_rgba(22,163,74,0.12)]";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
className="flex max-h-[calc(100dvh-24px)] flex-col gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_18px_60px_rgba(15,23,42,0.18)] ring-[#e8eef7] sm:max-h-[min(92vh,760px)] sm:max-w-lg"
>
<div className="relative shrink-0 px-3 pb-2.5 pt-5 text-center sm:px-4">
<button
type="button"
onClick={() => onOpenChange(false)}
className="absolute right-3 top-3 inline-flex size-9 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
aria-label={t("actions.close")}
>
<XIcon className="size-5" />
</button>
<div
className={cn(
"mx-auto flex size-16 items-center justify-center rounded-full border-4 bg-white",
iconWrapClass,
)}
>
<ResultIcon className="size-11" strokeWidth={2.5} />
</div>
<DialogHeader className="mt-3 items-center gap-2">
<DialogTitle className="text-2xl font-black text-slate-950">
{isRefundedOrder
? t("hall.result.titleRefunded")
: isAllFailed
? t("hall.result.titleAllFailed")
: isPartial
? t("hall.result.titlePartial")
: isPendingOnly
? t("hall.result.titlePendingConfirm")
: t("hall.result.title")}
</DialogTitle>
{data ? (
<DialogDescription className="text-sm leading-relaxed text-slate-500">
{t("hall.result.draw")}{" "}
<span className="font-mono font-black text-[#e5002c]">{data.draw.draw_id}</span>
</DialogDescription>
) : null}
</DialogHeader>
</div>
<div
className="min-h-0 flex-1 overflow-y-auto border-y border-[#e8eef7] px-3 sm:px-4"
>
<div className="space-y-3 py-3">
{!data ? (
<p className="text-sm text-slate-500">
{t("hall.result.empty")}
</p>
) : (
<>
{isRefundedOrder ? (
<p className="rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-800">
{t("hall.result.refundedHint")}
</p>
) : null}
{isPendingOnly ? (
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
{t("hall.result.pendingConfirmHint")}
</p>
) : null}
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-4 text-center">
<p className="text-sm font-bold text-emerald-700">
{t("hall.result.successCount")}
</p>
<p className="mt-2 text-4xl font-black tabular-nums text-emerald-600">{totalSuccess}</p>
</div>
<div className="rounded-lg border border-rose-100 bg-rose-50 px-3 py-4 text-center">
<p className="text-sm font-bold text-rose-600">
{t("hall.result.failureCount")}
</p>
<p className="mt-2 text-4xl font-black tabular-nums text-[#e5002c]">{totalFailure}</p>
</div>
</div>
<div className="flex items-center justify-between gap-3 rounded-lg border border-[#dbe7ff] bg-[#f4f8ff] px-3 py-2.5">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[#0755c7] text-white">
<ClipboardList className="size-5" />
</span>
<span className="truncate text-sm font-black text-[#0b3f96]">
{t("hall.result.actual")}
</span>
</div>
<span className="shrink-0 font-mono text-base font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode)}
</span>
</div>
<div className="grid grid-cols-2 gap-2 rounded-lg border border-[#e8eef7] bg-white px-3 py-2.5 text-xs text-slate-600">
<p>
{t("hall.result.orderNo")}:{" "}
<span className="font-mono font-black text-[#0b3f96]">{data.order_no}</span>
</p>
<p>
{creditMode
? t("hall.result.creditBalanceAfter", { defaultValue: "可用信用" })
: t("hall.result.balanceAfter")}
:{" "}
<span className="font-semibold text-slate-950">
{formatMinorAsCurrency(data.balance_after, currencyCode)}
</span>
</p>
</div>
{jackpotEnabled ? (
<div className="rounded-lg border border-[#d9ecff] bg-[#f4f9ff] px-3 py-3 text-sm leading-relaxed text-[#0b3f96]">
<p className="font-black">{t("hall.jackpotParticipation.title")}</p>
<p className="mt-1 text-xs">{t("hall.jackpotParticipation.resultDescription")}</p>
</div>
) : null}
<div className="space-y-2">
<p className="text-sm font-black text-[#e5002c]">
{t("hall.result.items")}
</p>
<div className="overflow-x-auto rounded-xl border border-[#dfe8f6]">
<table className="min-w-[520px] w-full border-collapse text-xs">
<thead className="bg-[#f4f7fd] text-[#304f86]">
<tr>
<th className="w-10 border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.table.no")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.result.number")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("orders.play")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.providers.title")}
</th>
<th className="px-2 py-2.5 text-center font-black">
{t("hall.result.actualDeduct")}
</th>
</tr>
</thead>
<tbody>
{successItems.map((item, index) => (
<tr
key={`${item.ticket_no}-${index}`}
className="border-t border-[#e8eef7] bg-white"
>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-black text-[#304f86]">
{index + 1}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center">
<span className="block font-mono text-base font-black text-slate-950">
{item.number}
</span>
<span className="block truncate text-[11px] text-slate-400">
{item.ticket_no}
</span>
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold text-[#0755c7]">
{playLabel(item.play_code, t)}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center">
<span className="inline-flex rounded-full bg-[#eef5ff] px-2 py-0.5 font-black text-[#0b56b7]">
{providerLabel(item.provider_name, item.provider_code)}
</span>
</td>
<td className="px-2 py-3 text-center font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(item.actual_deduct_amount, currencyCode)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{failedItems.length === 0 && pendingItems.length === 0 ? (
<div className="rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-3 text-sm font-semibold text-emerald-700">
{t("hall.result.noFailures")}
</div>
) : (
<div className="space-y-2 rounded-lg border border-rose-100 bg-rose-50 px-3 py-3">
{failedItems.length > 0 ? (
<>
<p className="text-sm font-black text-[#e5002c]">
{t("hall.result.failedItems")}
</p>
{failedItems.map((item, index) => (
<div
key={`${item.ticket_no}-${index}`}
className="flex items-center justify-between gap-3 rounded-md bg-white px-3 py-2 text-xs"
>
<span className="min-w-0 truncate font-semibold text-slate-700">
<span className="font-mono font-black text-slate-950">{item.number}</span>{" "}
{playLabel(item.play_code, t)} · {providerLabel(item.provider_name, item.provider_code)}
</span>
<span className="shrink-0 font-bold text-[#e5002c]">
{item.fail_reason_text ?? item.fail_reason_code ?? t("hall.result.failed")}
</span>
</div>
))}
</>
) : null}
{pendingItems.length > 0 ? (
<p className="text-xs text-amber-800">
{t("hall.result.pendingConfirmLines", { count: pendingItems.length })}
</p>
) : null}
</div>
)}
</div>
</>
)}
</div>
</div>
<div className="grid shrink-0 grid-cols-2 gap-2.5 border-t border-[#e8eef7] bg-white p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]">
<Button
type="button"
nativeButton={false}
variant="outline"
className="h-12 rounded-lg border-[#8dadf0] bg-white text-base font-black text-[#0b3f96] hover:bg-[#f4f8ff]"
render={<Link href="/orders" />}
>
<ClipboardList className="size-5" />
{t("hall.result.viewBets")}
</Button>
<Button
type="button"
onClick={() => onOpenChange(false)}
className="h-12 rounded-lg border-0 bg-[#07459f] text-base font-black text-white shadow-[0_10px_24px_rgba(7,69,159,0.24)] hover:bg-[#063b88]"
>
<Ticket className="size-5" />
{t("hall.result.continueBetting")}
</Button>
</div>
</DialogContent>
</Dialog>
);
}