feat(api, ui, i18n): 增强奖池管理与钱包功能

新增奖池余额调整与调整记录查询相关 API,提升后台对奖池的管理与控制能力。
更新奖池与钱包相关多语言文案,新增余额调整与转账完成提示信息,提升用户理解与反馈体验。
优化奖池管理相关 UI 组件,新增余额调整功能并改进页面布局,提升操作易用性。
重构相关组件以整合新功能,并进一步优化后台管理界面的整体用户体验。
This commit is contained in:
2026-05-26 14:59:41 +08:00
parent 60271d87fb
commit eb83bcf360
23 changed files with 881 additions and 228 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Copy } from "lucide-react";
import { Copy, Loader2, MoreHorizontal } from "lucide-react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -11,12 +11,20 @@ import {
getAdminWalletTransactions,
reverseTransferOrder,
manuallyProcessTransferOrder,
completeTransferInCredit,
} from "@/api/admin-wallet";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
@@ -44,9 +52,11 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useExportLabels } from "@/hooks/use-export-labels";
import { formatAdminMinorUnits } from "@/lib/money";
import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors";
import type {
AdminPlayerWalletsData,
AdminTransferOrderItem,
AdminTransferOrderListData,
AdminWalletTxnListData,
} from "@/types/api/admin-wallet";
@@ -213,8 +223,29 @@ function canReverseTransferOrder(
return canWriteWallet && (row.can_reverse ?? row.status === "pending_reconcile");
}
function canCompleteTransferInCredit(
row: {
direction: string;
status: string;
fail_reason?: string | null;
external_ref_no?: string | null;
can_complete_credit?: boolean;
},
canWriteWallet: boolean,
): boolean {
return (
canWriteWallet &&
(row.can_complete_credit ??
(row.direction === "in" &&
row.status === "pending_reconcile" &&
row.fail_reason === "lottery_credit_failed" &&
Boolean(row.external_ref_no?.trim())))
);
}
function canManuallyProcessTransferOrder(
row: {
direction?: string;
status: string;
can_manually_process?: boolean;
},
@@ -222,7 +253,80 @@ function canManuallyProcessTransferOrder(
): boolean {
return (
canWriteWallet &&
(row.can_manually_process ?? ["processing", "failed", "pending_reconcile"].includes(row.status))
(row.can_manually_process ??
(["processing", "failed", "pending_reconcile"].includes(row.status) &&
!(row.direction === "out" && row.status === "pending_reconcile")))
);
}
type TransferOrderRowActionsProps = {
row: AdminTransferOrderItem;
canWriteWallet: boolean;
busy: boolean;
onCompleteCredit: (transferNo: string) => void;
onReverse: (transferNo: string) => void;
onManualProcess: (transferNo: string) => void;
t: (key: string) => string;
};
function TransferOrderRowActions({
row,
canWriteWallet,
busy,
onCompleteCredit,
onReverse,
onManualProcess,
t,
}: TransferOrderRowActionsProps): React.ReactElement {
const showComplete = canCompleteTransferInCredit(row, canWriteWallet);
const showReverse = canReverseTransferOrder(row, canWriteWallet);
const showManual = canManuallyProcessTransferOrder(row, canWriteWallet);
if (!showComplete && !showReverse && !showManual) {
return <span className="text-xs text-muted-foreground"></span>;
}
return (
<DropdownMenu>
<DropdownMenuTrigger
disabled={busy}
aria-label={t("actionsMenuAriaLabel")}
className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }),
"text-muted-foreground hover:text-foreground",
)}
>
{busy ? (
<Loader2 className="size-4 animate-spin" aria-hidden />
) : (
<MoreHorizontal className="size-4" aria-hidden />
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[11rem]">
{showComplete ? (
<DropdownMenuItem disabled={busy} onClick={() => onCompleteCredit(row.transfer_no)}>
{t("completeCredit")}
</DropdownMenuItem>
) : null}
{showManual ? (
<DropdownMenuItem disabled={busy} onClick={() => onManualProcess(row.transfer_no)}>
{t("manualProcess")}
</DropdownMenuItem>
) : null}
{showReverse ? (
<>
{showComplete || showManual ? <DropdownMenuSeparator /> : null}
<DropdownMenuItem
variant="destructive"
disabled={busy}
onClick={() => onReverse(row.transfer_no)}
>
{t("reverse")}
</DropdownMenuItem>
</>
) : null}
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -280,6 +384,14 @@ export function TransferOrdersPanel(): React.ReactElement {
doAction(transferNo, () => manuallyProcessTransferOrder(transferNo), t("manualProcessSuccess")),
});
const handleCompleteCredit = (transferNo: string) =>
requestConfirm({
title: t("confirm.completeCreditTitle"),
description: t("confirm.completeCreditDescription", { transferNo }),
onConfirm: () =>
doAction(transferNo, () => completeTransferInCredit(transferNo), t("completeCreditSuccess")),
});
const load = useCallback(async () => {
setLoading(true);
setErr(null);
@@ -464,7 +576,7 @@ export function TransferOrdersPanel(): React.ReactElement {
<TableHead className="min-w-0 max-w-[14rem]">{t("failReason")}</TableHead>
<TableHead className="min-w-0 whitespace-normal leading-tight">{t("requestTime")}</TableHead>
<TableHead className="min-w-0 whitespace-normal leading-tight">{t("finishedTime")}</TableHead>
<TableHead className="w-24">{t("actions")}</TableHead>
<TableHead className="w-12 text-center">{t("actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -506,36 +618,18 @@ export function TransferOrdersPanel(): React.ReactElement {
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
{formatTs(row.finished_at)}
</TableCell>
<TableCell>
{canReverseTransferOrder(row, canWriteWallet) ||
canManuallyProcessTransferOrder(row, canWriteWallet) ? (
<div className="flex flex-col gap-1">
{canReverseTransferOrder(row, canWriteWallet) ? (
<Button
size="sm"
variant="destructive"
className="text-xs"
disabled={actionLoading.has(row.transfer_no)}
onClick={() => handleReverse(row.transfer_no)}
>
{actionLoading.has(row.transfer_no) ? t("processing") : t("reverse")}
</Button>
) : null}
{canManuallyProcessTransferOrder(row, canWriteWallet) ? (
<Button
size="sm"
variant="outline"
className="text-xs"
disabled={actionLoading.has(row.transfer_no)}
onClick={() => handleManuallyProcess(row.transfer_no)}
>
{actionLoading.has(row.transfer_no) ? t("processing") : t("manualProcess")}
</Button>
) : null}
</div>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
<TableCell className="text-center align-middle">
<div className="flex justify-center">
<TransferOrderRowActions
row={row}
canWriteWallet={canWriteWallet}
busy={actionLoading.has(row.transfer_no)}
onCompleteCredit={handleCompleteCredit}
onReverse={handleReverse}
onManualProcess={handleManuallyProcess}
t={t}
/>
</div>
</TableCell>
</TableRow>
))