feat: 新增玩家管理模块,完善钱包转账单操作能力
1. 重构玩家模块导航与元信息,将原玩家查询改为玩家列表 2. 新增完整的玩家CRUD API与前端管理页面,支持搜索、新建、编辑、删除玩家 3. 为转账单新增冲正与人工处理操作,补充状态显示与对应枚举 4. 优化用户列表表格空值展示与样式细节 5. 调整钱包子导航,移除旧的玩家钱包入口
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
getAdminPlayerWallets,
|
||||
getAdminTransferOrders,
|
||||
getAdminWalletTransactions,
|
||||
reverseTransferOrder,
|
||||
manuallyProcessTransferOrder,
|
||||
} from "@/api/admin-wallet";
|
||||
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
@@ -97,9 +99,31 @@ function statusBadgeVariant(
|
||||
if (status === "success" || status === "posted") return "secondary";
|
||||
if (status === "failed") return "destructive";
|
||||
if (status === "pending_reconcile") return "outline";
|
||||
if (status === "reversed" || status === "manually_processed") return "outline";
|
||||
return "default";
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case "processing":
|
||||
return "处理中";
|
||||
case "success":
|
||||
return "成功";
|
||||
case "failed":
|
||||
return "失败";
|
||||
case "pending_reconcile":
|
||||
return "待对账";
|
||||
case "reversed":
|
||||
return "已冲正";
|
||||
case "manually_processed":
|
||||
return "已人工处理";
|
||||
case "posted":
|
||||
return "已记账";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
type TransferFilters = {
|
||||
playerId: string;
|
||||
playerAccount: string;
|
||||
@@ -160,6 +184,7 @@ const WALLET_TXN_BIZ_OPTIONS: { value: string; label: string }[] = [
|
||||
const WALLET_TXN_STATUS_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "posted", label: "已记账" },
|
||||
{ value: "pending_reconcile", label: "待对账" },
|
||||
{ value: "reversed", label: "已冲正" },
|
||||
];
|
||||
|
||||
/** 与 {@see TransferOrderListController::ALLOWED_STATUS} 一致 */
|
||||
@@ -168,6 +193,8 @@ const TRANSFER_ORDER_STATUS_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "success", label: "成功" },
|
||||
{ value: "failed", label: "失败" },
|
||||
{ value: "pending_reconcile", label: "待对账" },
|
||||
{ value: "reversed", label: "已冲正" },
|
||||
{ value: "manually_processed", label: "已人工处理" },
|
||||
];
|
||||
|
||||
/** Base UI 的 SelectValue 会直接显示 `value`,需把哨兵转成「不限」、其余转成选项文案 */
|
||||
@@ -191,6 +218,34 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
const [perPage, setPerPage] = useState(20);
|
||||
const [draft, setDraft] = useState<TransferFilters>(emptyTransferFilters);
|
||||
const [applied, setApplied] = useState<TransferFilters>(emptyTransferFilters);
|
||||
const [actionLoading, setActionLoading] = useState<Set<string>>(new Set());
|
||||
|
||||
const doAction = async (
|
||||
transferNo: string,
|
||||
fn: () => Promise<unknown>,
|
||||
successMsg: string,
|
||||
) => {
|
||||
setActionLoading((prev) => new Set(prev).add(transferNo));
|
||||
try {
|
||||
await fn();
|
||||
toast.success(successMsg);
|
||||
void load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "操作失败");
|
||||
} finally {
|
||||
setActionLoading((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(transferNo);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReverse = (transferNo: string) =>
|
||||
doAction(transferNo, () => reverseTransferOrder(transferNo), "冲正成功");
|
||||
|
||||
const handleManuallyProcess = (transferNo: string) =>
|
||||
doAction(transferNo, () => manuallyProcessTransferOrder(transferNo), "人工处理成功");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -405,7 +460,7 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
{formatMinorUnits(row.amount, row.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusBadgeVariant(row.status)}>{row.status}</Badge>
|
||||
<Badge variant={statusBadgeVariant(row.status)}>{statusLabel(row.status)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[14rem] whitespace-normal break-words text-xs text-muted-foreground">
|
||||
{row.fail_reason?.trim() ? row.fail_reason : "—"}
|
||||
@@ -416,7 +471,32 @@ 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 className="text-xs text-muted-foreground">—</TableCell>
|
||||
<TableCell>
|
||||
{row.status === "pending_reconcile" ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={actionLoading.has(row.transfer_no)}
|
||||
onClick={() => handleReverse(row.transfer_no)}
|
||||
>
|
||||
{actionLoading.has(row.transfer_no) ? "处理中…" : "冲正"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={actionLoading.has(row.transfer_no)}
|
||||
onClick={() => handleManuallyProcess(row.transfer_no)}
|
||||
>
|
||||
{actionLoading.has(row.transfer_no) ? "处理中…" : "人工处理"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
@@ -666,13 +746,12 @@ export function WalletTxnsPanel(): React.ReactElement {
|
||||
<TableHead className="min-w-0 whitespace-normal leading-tight">
|
||||
完成时间
|
||||
</TableHead>
|
||||
<TableHead className="w-24">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-muted-foreground">
|
||||
<TableCell colSpan={8} className="text-muted-foreground">
|
||||
无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -697,7 +776,7 @@ export function WalletTxnsPanel(): React.ReactElement {
|
||||
{row.amount} ({row.direction === 1 ? "入" : "出"})
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusBadgeVariant(row.status)}>{row.status}</Badge>
|
||||
<Badge variant={statusBadgeVariant(row.status)}>{statusLabel(row.status)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
|
||||
{formatTs(row.created_at)}
|
||||
@@ -705,7 +784,6 @@ export function WalletTxnsPanel(): React.ReactElement {
|
||||
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
|
||||
{formatTs(row.updated_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">—</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -13,16 +13,9 @@ const RECONCILE_PERMS = [
|
||||
"prd.wallet_reconcile.view_cs",
|
||||
] as const;
|
||||
|
||||
const USER_PERMS = [
|
||||
"prd.users.manage",
|
||||
"prd.users.view_finance",
|
||||
"prd.users.view_cs",
|
||||
] as const;
|
||||
|
||||
const tabs: { href: string; label: string; requiredAny: readonly string[] }[] = [
|
||||
{ href: "/admin/wallet/transactions", label: "钱包流水", requiredAny: RECONCILE_PERMS },
|
||||
{ href: "/admin/wallet/transfer-orders", label: "转账单", requiredAny: RECONCILE_PERMS },
|
||||
{ href: "/admin/wallet/player", label: "玩家钱包", requiredAny: USER_PERMS },
|
||||
];
|
||||
|
||||
export function WalletSubnav(): React.ReactElement {
|
||||
|
||||
Reference in New Issue
Block a user