feat: 增强钱包 API 与玩家会话管理
- 新增钱包 API 函数:getWalletLogs(获取钱包日志)、postWalletTransferIn(充值)及 postWalletTransferOut(提现) - 更新钱包相关类型定义,提升类型安全性 - 改进玩家会话管理:若当前无玩家资料,则自动拉取玩家信息 - 增强入口网关对过期会话的错误处理能力 - 更新 UI 组件,以适配新的结构与功能
This commit is contained in:
235
src/features/wallet/wallet-screen.tsx
Normal file
235
src/features/wallet/wallet-screen.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { Wallet } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { getWalletBalance, getWalletLogs } from "@/api/wallet";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
TransferInDialog,
|
||||
TransferOutDialog,
|
||||
} from "@/features/wallet/wallet-transfer-dialogs";
|
||||
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import type { WalletLogsData } from "@/types/api/wallet-logs";
|
||||
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||
|
||||
export function WalletScreen() {
|
||||
const profile = usePlayerSessionStore((s) => s.profile);
|
||||
|
||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||
const [logs, setLogs] = useState<WalletLogsData | null>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const currency = useMemo(() => {
|
||||
return (
|
||||
balance?.currency_code ??
|
||||
profile?.default_currency ??
|
||||
"NPR"
|
||||
).toUpperCase();
|
||||
}, [balance?.currency_code, profile?.default_currency]);
|
||||
|
||||
const fetchPassRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
setError(null);
|
||||
if (fetchPassRef.current) {
|
||||
setLoading(true);
|
||||
fetchPassRef.current = false;
|
||||
} else {
|
||||
setLogsLoading(true);
|
||||
}
|
||||
try {
|
||||
const b = await getWalletBalance();
|
||||
if (cancelled) return;
|
||||
setBalance(b);
|
||||
const L = await getWalletLogs({
|
||||
page: 1,
|
||||
size: 50,
|
||||
type: filter || undefined,
|
||||
});
|
||||
if (cancelled) return;
|
||||
setLogs(L);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
setError(formatWalletClientError(e));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
setLogsLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [filter]);
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
setError(null);
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const b = await getWalletBalance();
|
||||
setBalance(b);
|
||||
const L = await getWalletLogs({
|
||||
page: 1,
|
||||
size: 50,
|
||||
type: filter || undefined,
|
||||
});
|
||||
setLogs(L);
|
||||
} catch (e) {
|
||||
setError(formatWalletClientError(e));
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold tracking-tight">彩票钱包</h1>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<Link
|
||||
href="/wallet/transfer-in"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "secondary", size: "sm" }),
|
||||
"text-xs",
|
||||
)}
|
||||
>
|
||||
转入页
|
||||
</Link>
|
||||
<Link
|
||||
href="/wallet/transfer-out"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "secondary", size: "sm" }),
|
||||
"text-xs",
|
||||
)}
|
||||
>
|
||||
转出页
|
||||
</Link>
|
||||
<Link
|
||||
href="/wallet/logs"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "secondary", size: "sm" }),
|
||||
"text-xs",
|
||||
)}
|
||||
>
|
||||
流水页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/hall"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"shrink-0 self-start",
|
||||
)}
|
||||
>
|
||||
返回大厅
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<Card className="border-destructive/40">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive">加载失败</CardTitle>
|
||||
<CardDescription>{error}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button type="button" onClick={() => void refreshAll()}>
|
||||
重试
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Wallet className="size-5 opacity-80" aria-hidden />
|
||||
余额
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{loading ? (
|
||||
<Skeleton className="h-12 w-full max-w-xs rounded-lg" />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">彩票钱包余额</p>
|
||||
<p className="font-heading text-2xl font-semibold tabular-nums text-[#52c41a]">
|
||||
{formatMinorAsCurrency(
|
||||
balance?.balance ?? 0,
|
||||
currency,
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
可用{" "}
|
||||
{formatMinorAsCurrency(
|
||||
balance?.available_balance ?? 0,
|
||||
currency,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
主站钱包余额{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{balance?.main_balance == null
|
||||
? "—(待接入主站)"
|
||||
: formatMinorAsCurrency(balance.main_balance, currency)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<TransferInDialog
|
||||
idPrefix="wallet-"
|
||||
currency={currency}
|
||||
lotteryMinor={Number(balance?.balance ?? 0)}
|
||||
onSuccess={refreshAll}
|
||||
triggerVariant="wallet"
|
||||
/>
|
||||
<TransferOutDialog
|
||||
idPrefix="wallet-"
|
||||
currency={currency}
|
||||
availableMinor={Number(balance?.available_balance ?? 0)}
|
||||
onSuccess={refreshAll}
|
||||
triggerVariant="wallet"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<WalletLogsBlock
|
||||
logs={logs}
|
||||
logsLoading={loading || logsLoading}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
currency={currency}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user