feat: 增强注单处理逻辑并优化用户反馈体验

新增注单错误处理逻辑,支持已退款及待确认状态的异常场景处理。
更新 HallBetResultDialog:针对部分失败与已退款订单显示对应提示信息。
优化注单分组逻辑,新增订单状态处理,提升整体订单管理能力。
新增订单状态与用户通知相关多语言翻译,进一步提升用户体验。
This commit is contained in:
2026-05-26 16:32:53 +08:00
parent 51b2a36cc5
commit ab81da3199
19 changed files with 373 additions and 142 deletions

View File

@@ -25,6 +25,9 @@ type HallBetResultDialogProps = {
jackpotEnabled?: boolean;
};
const SUCCESS_ITEM_STATUSES = new Set(["pending_draw", "placed"]);
const FAILURE_ITEM_STATUSES = new Set(["failed", "refunded"]);
export function HallBetResultDialog({
open,
onOpenChange,
@@ -34,18 +37,41 @@ export function HallBetResultDialog({
}: HallBetResultDialogProps) {
const { t } = useTranslation("player");
// 成功注项状态为 pending_draw待开奖不是 success
const successItems = data?.items.filter((item) => item.status !== "failed") ?? [];
const failedItems = data?.items.filter((item) => item.status === "failed") ?? [];
const successItems =
data?.items.filter((item) => SUCCESS_ITEM_STATUSES.has(item.status)) ?? [];
const failedItems =
data?.items.filter((item) => 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 isPartial = totalSuccess > 0 && totalFailure > 0;
const isAllFailed = totalFailure > 0 && totalSuccess === 0;
const pendingConfirmCount =
data?.summary.pending_confirm_count ?? pendingItems.length;
const ResultIcon = isAllFailed ? XCircle : isPartial ? AlertTriangle : CheckCircle2;
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
: 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)]";
@@ -74,11 +100,15 @@ export function HallBetResultDialog({
</div>
<DialogHeader className="mt-3 items-center gap-2">
<DialogTitle className="text-2xl font-black text-slate-950">
{isAllFailed
? t("hall.result.titleAllFailed")
: isPartial
? t("hall.result.titlePartial")
: t("hall.result.title")}
{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">
@@ -99,6 +129,16 @@ export function HallBetResultDialog({
</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">
@@ -196,29 +236,38 @@ export function HallBetResultDialog({
</tbody>
</table>
</div>
{totalFailure === 0 ? (
{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">
<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)}
</span>
<span className="shrink-0 font-bold text-[#e5002c]">
{item.fail_reason_text ?? item.fail_reason_code ?? t("hall.result.failed")}
</span>
</div>
))}
{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)}
</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>