feat: 增强钱包 API 与玩家会话管理

- 新增钱包 API 函数:getWalletLogs(获取钱包日志)、postWalletTransferIn(充值)及 postWalletTransferOut(提现)
- 更新钱包相关类型定义,提升类型安全性
- 改进玩家会话管理:若当前无玩家资料,则自动拉取玩家信息
- 增强入口网关对过期会话的错误处理能力
- 更新 UI 组件,以适配新的结构与功能
This commit is contained in:
2026-05-09 15:22:08 +08:00
parent 14c297fe1a
commit 7743c14e83
28 changed files with 1719 additions and 33 deletions

View File

@@ -0,0 +1,34 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { HallWalletStrip } from "@/features/hall/hall-wallet-strip";
/**
* 下注大厅:顶部钱包条对齐高保真稿;以下为期号/表格占位。
*/
export function HallScreen() {
return (
<div className="flex flex-col gap-5">
<HallWalletStrip />
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardDescription>
Issue No.2D/3D/4D Submit Bet §4.2
</CardDescription>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
WebSocket PRD §2
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,118 @@
"use client";
import { Wallet } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { getWalletBalance } from "@/api/wallet";
import { buttonVariants } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
TransferInDialog,
TransferOutDialog,
} from "@/features/wallet/wallet-transfer-dialogs";
import { formatMinorAsCurrency } from "@/lib/money";
import { cn } from "@/lib/utils";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import type { WalletBalanceData } from "@/types/api/wallet-balance";
/**
* 高保真稿:大厅顶部红卡 + Transfer In/ Transfer Out白底红边§4.2
*/
export function HallWalletStrip() {
const profile = usePlayerSessionStore((s) => s.profile);
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
const [loading, setLoading] = useState(true);
const currency = useMemo(
() =>
(balance?.currency_code ?? profile?.default_currency ?? "NPR").toUpperCase(),
[balance?.currency_code, profile?.default_currency],
);
const refresh = useCallback(async () => {
const b = await getWalletBalance();
setBalance(b);
}, []);
useEffect(() => {
let c = false;
void (async () => {
setLoading(true);
try {
await refresh();
} finally {
if (!c) setLoading(false);
}
})();
return () => {
c = true;
};
}, [refresh]);
const lotteryMinor = Number(balance?.balance ?? 0);
const availableMinor = Number(balance?.available_balance ?? 0);
return (
<section className="space-y-2" aria-label="Wallet balance">
<div
className={cn(
"relative overflow-hidden rounded-2xl bg-gradient-to-br from-[#dc2626] to-[#991b1b] px-4 py-3.5 text-white shadow-md",
"ring-1 ring-black/10",
)}
>
<div className="flex items-start gap-3">
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-white/15">
<Wallet className="size-6 text-white" aria-hidden />
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium uppercase tracking-wide text-white/80">
Wallet Balance
</p>
{loading ? (
<Skeleton className="mt-2 h-8 w-40 rounded-md bg-white/20" />
) : (
<p className="font-heading text-2xl font-bold tabular-nums tracking-tight">
{formatMinorAsCurrency(lotteryMinor, currency)}
</p>
)}
</div>
<Link
href="/wallet"
className={cn(
buttonVariants({
variant: "secondary",
size: "sm",
}),
"shrink-0 border-0 bg-white/15 text-xs text-white hover:bg-white/25",
)}
>
</Link>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<TransferInDialog
idPrefix="hall-"
triggerVariant="hall"
triggerLabel="Transfer In"
triggerClassName="w-full min-w-0"
currency={currency}
lotteryMinor={lotteryMinor}
onSuccess={refresh}
/>
<TransferOutDialog
idPrefix="hall-"
triggerVariant="hall"
triggerLabel="Transfer Out"
triggerClassName="w-full min-w-0"
currency={currency}
availableMinor={availableMinor}
onSuccess={refresh}
/>
</div>
</section>
);
}