Files
lotteryFront/src/features/hall/hall-bet-preview-dialog.tsx

321 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
AlertTriangleIcon,
CheckCircle2,
LoaderCircle,
WalletCards,
XIcon,
} from "lucide-react";
import Image from "next/image";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
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 type { TicketPreviewData, TicketPreviewWarning } from "@/types/api/ticket";
type HallBetPreviewDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
currencyCode: string;
data: TicketPreviewData | null;
placing: boolean;
/** 界面 §4.2:封盘后禁止提交,主按钮文案为「已封盘」 */
allowSubmit?: boolean;
onConfirmPlace: () => void;
};
function WarningsBlock({ warnings }: { warnings: TicketPreviewWarning[] }) {
const { t } = useTranslation("player");
if (warnings.length === 0) return null;
return (
<Alert className="rounded-xl border-[#ffd7b8] bg-[#fff7ed] p-3 text-[#b45309]">
<AlertTriangleIcon className="size-5" />
<AlertTitle className="text-sm font-black">{t("hall.preview.warningsTitle")}</AlertTitle>
<AlertDescription className="space-y-2">
<p className="text-xs leading-relaxed text-[#92400e]">
{t("hall.preview.warningsDescription")}
</p>
<ul className="space-y-1 text-xs text-slate-700">
{warnings.map((w, i) => (
<li key={`${w.number_4d}-${i}`}>
<span className="font-mono font-black text-[#e5002c]">{w.number_4d}</span> · {w.message}
</li>
))}
</ul>
</AlertDescription>
</Alert>
);
}
function SubmittingPanel() {
const { t } = useTranslation("player");
return (
<DialogContent
showCloseButton={false}
className="max-w-[340px] overflow-hidden rounded-2xl border border-white/70 bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
>
<div className="px-8 py-8 text-center">
<Image
src="/entry/image6.png"
alt=""
width={150}
height={119}
className="mx-auto h-[118px] w-[150px] object-contain"
priority
aria-hidden
/>
<div className="mx-auto mt-2 h-2 w-44 overflow-hidden rounded-full bg-[#dbe3f1]">
<div className="h-full w-2/3 rounded-full bg-[#0755c7] shadow-[0_0_14px_rgba(7,85,199,0.45)]" />
</div>
<DialogHeader className="mt-8 items-center gap-2">
<DialogTitle className="text-xl font-black text-slate-950">
{t("hall.preview.processingTitle", { defaultValue: "正在提交下注" })}
</DialogTitle>
<DialogDescription className="max-w-[220px] text-center text-sm leading-relaxed text-slate-500">
{t("hall.preview.processingDescription", { defaultValue: "请勿关闭页面或返回上一页。" })}
</DialogDescription>
</DialogHeader>
<div className="mt-7 inline-flex items-center gap-2 text-sm font-semibold text-slate-500">
<LoaderCircle className="size-4 animate-spin text-[#0755c7]" />
{t("hall.preview.processingProgress", { defaultValue: "正在处理注单..." })}
</div>
</div>
</DialogContent>
);
}
/**
* 预览弹窗 + 提交确认(产品文档 §10.1.2:预览不下单,确认后 place
*/
export function HallBetPreviewDialog({
open,
onOpenChange,
currencyCode,
data,
placing,
allowSubmit = true,
onConfirmPlace,
}: HallBetPreviewDialogProps) {
const { t } = useTranslation("player");
const summary = data?.summary;
const lines = data?.lines ?? [];
useEffect(() => {
if (open && !placing && !data) {
onOpenChange(false);
}
}, [data, onOpenChange, open, placing]);
if (placing) {
return (
<Dialog open={open} onOpenChange={() => {}}>
<SubmittingPanel />
</Dialog>
);
}
if (!data) {
return null;
}
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-4 pb-3 pt-5 sm:px-5">
<button
type="button"
onClick={() => onOpenChange(false)}
disabled={placing}
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 disabled:opacity-50"
aria-label={t("actions.close", { defaultValue: "关闭" })}
>
<XIcon className="size-5" />
</button>
<DialogHeader className="pr-10">
<div className="mb-2 flex items-center gap-3">
<span className="flex size-11 items-center justify-center rounded-full bg-[#fff0f3] text-[#e5002c]">
<WalletCards className="size-6" />
</span>
<DialogTitle className="text-xl font-black text-slate-950">
{t("hall.preview.title")}
</DialogTitle>
</div>
<DialogDescription className="text-sm leading-relaxed text-slate-500">
{t("hall.preview.description")}
</DialogDescription>
</DialogHeader>
{!allowSubmit ? (
<Alert className="mt-3 border-[#ff4d4f]/35 bg-[#ff4d4f]/8 text-[#ff4d4f]">
<AlertTriangleIcon />
<AlertTitle>{t("hall.preview.sealedTitle")}</AlertTitle>
<AlertDescription className="text-xs leading-relaxed">
{t("hall.preview.sealedDescription")}
</AlertDescription>
</Alert>
) : null}
</div>
<div
className="min-h-0 flex-1 overflow-y-auto border-y border-[#e8eef7] px-4 sm:px-5"
>
<div className="space-y-4 py-4">
{!data ? (
<p className="text-sm text-slate-500">{t("hall.preview.empty")}</p>
) : (
<>
<div className="flex flex-wrap items-center gap-2 text-sm">
<span className="text-slate-500">{t("hall.preview.draw")}:</span>
<span className="font-mono font-black text-[#e5002c]">{data.draw.draw_id}</span>
<span className="rounded-full bg-[#edf4ff] px-2 py-0.5 text-xs font-bold text-[#0b3f96]">
{data.draw.status}
</span>
</div>
<div className="overflow-x-auto rounded-xl border border-[#dfe8f6]">
<table className="min-w-[640px] w-full border-collapse text-xs">
<thead className="bg-[#f4f7fd] text-[#304f86]">
<tr>
<th className="w-10 border-r border-[#dfe8f6] px-2 py-3 text-center font-black">No.</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.result.number", { defaultValue: "号码" })}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("orders.play", { defaultValue: "玩法" })}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.amount")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.rebate")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.estimatedMax")}
</th>
<th className="px-2 py-3 text-center font-black">
{t("hall.preview.actual")}
</th>
</tr>
</thead>
<tbody>
{lines.map((ln) => (
<tr key={ln.client_line_no} className="border-t border-[#e8eef7] bg-white">
<td className="border-r border-[#e8eef7] px-2 py-3 text-center">
<span className="inline-flex size-6 items-center justify-center rounded-full border border-[#dfe8f6] font-black text-[#304f86]">
{ln.client_line_no}
</span>
</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">{ln.number}</span>
<span className="text-[11px] font-bold text-[#0755c7]">{playLabel(ln.play_code, t)}</span>
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold text-slate-700">
{ln.play_code}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold tabular-nums">
{formatMinorAsCurrency(ln.total_bet_amount, currencyCode)}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold tabular-nums text-emerald-600">
-{formatMinorAsCurrency(ln.rebate_amount, currencyCode).replace(`${currencyCode} `, "")}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-black tabular-nums text-[#e5002c]">
{formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)}
</td>
<td className="px-2 py-3 text-center font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(ln.actual_deduct_amount, currencyCode)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{summary ? (
<div className="grid grid-cols-2 overflow-hidden rounded-xl border border-[#dfe8f6] bg-[#f8fbff] text-center text-xs sm:grid-cols-4">
<div className="border-r border-[#dfe8f6] px-2 py-3">
<p className="font-bold text-[#304f86]">{t("hall.preview.totalBet")}</p>
<p className="mt-1 font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(summary.total_bet_amount, currencyCode)}
</p>
</div>
<div className="border-r border-[#dfe8f6] px-2 py-3">
<p className="font-bold text-[#304f86]">{t("hall.preview.rebate")}</p>
<p className="mt-1 font-black tabular-nums text-emerald-600">
-
{formatMinorAsCurrency(summary.total_rebate_amount, currencyCode).replace(
`${currencyCode} `,
"",
)}
</p>
</div>
<div className="border-r border-[#dfe8f6] px-2 py-3">
<p className="font-bold text-[#304f86]">{t("hall.preview.actualDeduct")}</p>
<p className="mt-1 font-black tabular-nums text-[#e5002c]">
{formatMinorAsCurrency(summary.total_actual_deduct, currencyCode)}
</p>
</div>
<div className="px-2 py-3">
<p className="font-bold text-[#304f86]">{t("hall.preview.estimatedPayout")}</p>
<p className="mt-1 font-black tabular-nums text-[#e5002c]">
{formatMinorAsCurrency(summary.total_estimated_payout, currencyCode)}
</p>
</div>
</div>
) : null}
{data.warnings.length > 0 ? (
<WarningsBlock warnings={data.warnings} />
) : (
<div className="flex items-center gap-2 rounded-xl border border-emerald-100 bg-emerald-50 px-3 py-3 text-sm font-semibold text-emerald-700">
<CheckCircle2 className="size-5" />
{t("hall.preview.noWarnings", { defaultValue: "当前预览未发现明显风险。" })}
</div>
)}
</>
)}
</div>
</div>
<div className="grid shrink-0 grid-cols-2 gap-3 border-t border-[#e8eef7] bg-white p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] sm:p-5">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={placing}
className="h-12 rounded-lg border-[#ff3650] bg-white text-base font-black text-[#e5002c] hover:bg-[#fff5f6]"
>
{t("hall.preview.backEdit")}
</Button>
<Button
type="button"
onClick={onConfirmPlace}
disabled={!data || placing || !allowSubmit}
className="h-12 rounded-lg border-0 bg-[#e5002c] text-base font-black text-white shadow-[0_10px_24px_rgba(229,0,44,0.28)] hover:bg-[#d10028] disabled:bg-slate-300 disabled:shadow-none"
>
{placing
? t("hall.preview.submitting")
: allowSubmit
? t("hall.preview.confirmSubmit")
: t("hall.preview.sealedTitle")}
</Button>
</div>
</DialogContent>
</Dialog>
);
}