Some checks failed
lotteryfront CI / build (push) Has been cancelled
- 在getPlayEffective接口中增加provider_code参数支持 - 优化接口调用时对currency和provider_code的传递逻辑 - hall-bet-preview-dialog新增previewLineKey方法,解决列表渲染key冲突问题 - 修改投注和结果弹窗中开注商标题的国际化调用方式 - hall-betting-grid中增加选中开注商的默认逻辑,接口调用时传递provider_code - 优化provider名称显示逻辑,完善开注商选择及摘要文本显示 - 订单详情页中显示开注商名称,提升显示准确性 - 补充i18n中开注商相关文案,支持多语言环境 - 更新PlayEffectiveOddsSlice和PlayEffectivePayload类型,加入provider_code字段支持
326 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|