"use client"; import { ArrowDownLeft, ArrowUpRight, Wallet } from "lucide-react"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { getWalletBalance, getWalletLogs, getWalletSettlementBills } from "@/api/wallet"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { PlayerPanel } from "@/components/layout/player-panel"; import { TransferInDialog, TransferOutDialog, } from "@/features/wallet/wallet-transfer-dialogs"; import { PlayerMoneyDisplay } from "@/components/player-money-display"; import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; import { isCreditFundingPlayer } from "@/lib/player-funding-mode"; import { formatMinorAsCurrency } from "@/lib/money"; import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; import { formatWalletClientError } from "@/lib/wallet-api-error"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import type { WalletBalanceData } from "@/types/api/wallet-balance"; import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs"; import type { WalletSettlementBillItem, WalletSettlementBillsData, } from "@/types/api/wallet-settlement-bills"; import type { PlayerMeData } from "@/types/api/player-me"; const WALLET_LOGS_PAGE_SIZE = 10; type FundingModeView = "credit" | "wallet" | "unknown"; function scrollToWalletSection(id: string): void { if (typeof document === "undefined") return; document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); } function resolveFundingModeView( balance: WalletBalanceData | null, profile: PlayerMeData | null, ): FundingModeView { if (isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile)) { return "credit"; } if (balance?.funding_mode === "wallet" || profile?.funding_mode === "wallet") { return "wallet"; } return "unknown"; } function settlementStatusLabel( status: string, t: (key: string, options?: { defaultValue?: string }) => string, ): string { const fallback: Record = { pending_confirm: "待确认", confirmed: "待收付", partial_paid: "部分收付", overdue: "已逾期", settled: "已结清", }; return t(`wallet.settlementStatus.${status}`, { defaultValue: fallback[status] ?? status }); } function formatPeriodRange( item: WalletSettlementBillItem, settlementTimeZone?: string, ): string { const start = item.period_start ? (settlementTimeZone ? formatLotteryInstantInTimeZone(item.period_start, settlementTimeZone) : formatPlayerInstant(item.period_start)).slice(0, 10) : "—"; const end = item.period_end ? (settlementTimeZone ? formatLotteryInstantInTimeZone(item.period_end, settlementTimeZone) : formatPlayerInstant(item.period_end)).slice(0, 10) : "—"; return `${start} / ${end}`; } function CreditSettlementBillsBlock({ data, currency, settlementTimeZone, }: { data: WalletSettlementBillsData | null; currency: string; settlementTimeZone?: string; }) { const { t } = useTranslation("player"); const summary = data?.summary; const netPending = Number(summary?.net_pending ?? 0); const pendingCount = Number(summary?.pending_count ?? 0); const netDirection = netPending >= 0 ? "receivable" : "payable"; return (

{t("wallet.settlementTitle", { defaultValue: "我的账期账单" })}

{t("wallet.settlementHint", { defaultValue: "只显示未结清账单,中奖超出授信的部分在这里结算。" })}

{t("wallet.settlementPendingCount", { defaultValue: "{{count}}笔", count: pendingCount })}

{t("wallet.pendingReceivable", { defaultValue: "待收" })}

{formatMinorAsCurrency(summary?.pending_receivable ?? 0, currency)}

{t("wallet.pendingPayable", { defaultValue: "待付" })}

{formatMinorAsCurrency(summary?.pending_payable ?? 0, currency)}

{netDirection === "receivable" ? t("wallet.netReceivable", { defaultValue: "净待收" }) : t("wallet.netPayable", { defaultValue: "净待付" })} {formatMinorAsCurrency(Math.abs(netPending), currency)}
{data === null ? ( ) : data.items.length === 0 ? (

{t("wallet.noSettlementBills", { defaultValue: "暂无待结算账单" })}

) : (
    {data.items.map((item) => { const receivable = item.direction === "receivable"; return (
  • {receivable ? : }

    {receivable ? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" }) : t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })}

    {formatPeriodRange(item, settlementTimeZone)}

    {formatMinorAsCurrency(item.unpaid_amount, currency)}

    {settlementStatusLabel(item.status, t)}

  • ); })}
)}
); } export function WalletScreen() { const router = useRouter(); const searchParams = useSearchParams(); const { activeCurrency: currency } = useActivePlayerCurrency(); const { t } = useTranslation("player"); const [transferInOpen, setTransferInOpen] = useState(false); const [transferOutOpen, setTransferOutOpen] = useState(false); const [balance, setBalance] = useState(null); const [logs, setLogs] = useState(null); const [settlementBills, setSettlementBills] = useState(null); const [filter, setFilter] = useState(""); const [loading, setLoading] = useState(true); const [logsLoading, setLogsLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); const loadMoreRef = useRef(null); const filterInitializedRef = useRef(false); const prevFilterRef = useRef(""); const actionDeepLinkHandledRef = useRef(false); const sectionDeepLinkHandledRef = useRef(false); const profile = usePlayerSessionStore((s) => s.profile); const settlementTimeZone = profile?.settlement_timezone || "UTC"; const fundingModeView = resolveFundingModeView(balance, profile); const fundingModeKnown = fundingModeView !== "unknown"; const isCreditPlayer = fundingModeView === "credit"; useEffect(() => { if (actionDeepLinkHandledRef.current || loading) return; const action = searchParams.get("action"); if (action !== "transfer-in" && action !== "transfer-out") return; actionDeepLinkHandledRef.current = true; if (!isCreditPlayer) { queueMicrotask(() => { if (action === "transfer-in") { setTransferInOpen(true); } else { setTransferOutOpen(true); } }); } router.replace("/wallet", { scroll: false }); }, [isCreditPlayer, loading, router, searchParams]); useEffect(() => { if (sectionDeepLinkHandledRef.current || loading) return; const section = searchParams.get("section"); if (section !== "logs" && section !== "pending") return; sectionDeepLinkHandledRef.current = true; const targetId = section === "pending" ? "wallet-pending" : "wallet-logs"; const timer = window.setTimeout(() => { scrollToWalletSection(targetId); router.replace("/wallet", { scroll: false }); }, 120); return () => window.clearTimeout(timer); }, [isCreditPlayer, loading, router, searchParams]); const loadLogs = useCallback(async (targetPage = 1, append = false) => { const nextLogs = await getWalletLogs({ page: targetPage, size: WALLET_LOGS_PAGE_SIZE, type: filter || undefined, currency, }); setLogs((current) => append && current ? { ...nextLogs, items: [...current.items, ...nextLogs.items] } : nextLogs, ); return nextLogs; }, [currency, filter]); useEffect(() => { let cancelled = false; void (async () => { setError(null); setLoading(true); try { const [b, nextLogs] = await Promise.all([ getWalletBalance({ currency }), getWalletLogs({ page: 1, size: WALLET_LOGS_PAGE_SIZE, type: filter || undefined, currency, }), ]); if (cancelled) return; setBalance(b); setLogs(nextLogs); if (isCreditFundingPlayer(b) || isCreditFundingPlayer(profile)) { const bills = await getWalletSettlementBills(); if (!cancelled) setSettlementBills(bills); } else { setSettlementBills(null); } } catch (e) { if (!cancelled) { setError(formatWalletClientError(e, t)); } } finally { if (!cancelled) { setLoading(false); setLogsLoading(false); } } })(); return () => { cancelled = true; }; }, [currency, t]); // eslint-disable-line react-hooks/exhaustive-deps -- 币种切换整页刷新;流水筛选见下方 effect useEffect(() => { if (!filterInitializedRef.current) { filterInitializedRef.current = true; prevFilterRef.current = filter; return; } if (prevFilterRef.current === filter) { return; } prevFilterRef.current = filter; let cancelled = false; setError(null); setLogsLoading(true); void (async () => { try { const nextLogs = await getWalletLogs({ page: 1, size: WALLET_LOGS_PAGE_SIZE, type: filter || undefined, currency, }); if (!cancelled) { setLogs(nextLogs); } } catch (e) { if (!cancelled) { setError(formatWalletClientError(e, t)); } } finally { if (!cancelled) { setLogsLoading(false); } } })(); return () => { cancelled = true; }; }, [currency, filter, t]); const handleFilterChange = useCallback((value: string) => { if (value === filter) { return; } setLogs(null); setLogsLoading(true); setFilter(value); }, [filter]); const refreshAll = useCallback(async () => { setError(null); setLogsLoading(true); try { // 并行请求余额和日志 const [b] = await Promise.all([ getWalletBalance({ currency }), loadLogs(1, false), ]); setBalance(b); if (isCreditFundingPlayer(b) || isCreditFundingPlayer(profile)) { setSettlementBills(await getWalletSettlementBills()); } else { setSettlementBills(null); } } catch (e) { setError(formatWalletClientError(e, t)); } finally { setLogsLoading(false); setLoading(false); } }, [currency, loadLogs, profile, t]); useEffect(() => { const onCurrencyChange = () => void refreshAll(); window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange); return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange); }, [refreshAll]); const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false; const displayMinor = isCreditPlayer ? Number(balance?.available_balance ?? 0) : Number(balance?.balance ?? 0); const panelTitle = !fundingModeKnown ? t("wallet.loadingTitle", { defaultValue: "账户资金" }) : isCreditPlayer ? t("wallet.creditTitle", { defaultValue: "信用" }) : t("wallet.title"); const loadMore = useCallback(() => { if (!logs || !hasMore || loadingMore) return; setError(null); setLoadingMore(true); void loadLogs(logs.page + 1, true) .catch((e) => { setError(formatWalletClientError(e, t)); }) .finally(() => { setLoadingMore(false); }); }, [hasMore, loadLogs, loadingMore, logs, t]); useEffect(() => { const target = loadMoreRef.current; if (!target || loading || logsLoading || loadingMore || !hasMore) return; const observer = new IntersectionObserver( ([entry]) => { if (entry?.isIntersecting) { loadMore(); } }, { rootMargin: "160px" }, ); observer.observe(target); return () => observer.disconnect(); }, [hasMore, loadMore, loading, loadingMore, logsLoading]); return (
{error ? (

{error}

) : null}

{fundingModeKnown && isCreditPlayer ? t("wallet.creditAvailable", { defaultValue: "可用信用" }) : fundingModeKnown ? t("wallet.balance") : t("wallet.loadingBalance", { defaultValue: "正在加载" })}

{loading || !fundingModeKnown ? ( ) : ( )}

{!fundingModeKnown ? t("wallet.loadingAccountMode", { defaultValue: "正在确认资金模式…" }) : isCreditPlayer ? t("wallet.creditSummary", { defaultValue: "授信 {{limit}} · 已用 {{used}}", limit: formatMinorAsCurrency(balance?.credit_limit ?? 0, currency), used: formatMinorAsCurrency(balance?.used_credit ?? 0, currency), }) : t("wallet.available", { amount: formatMinorAsCurrency(balance?.available_balance ?? 0, currency), })}

{fundingModeKnown && isCreditPlayer ? ( ) : null} {fundingModeKnown && !isCreditPlayer ? (
) : null} {fundingModeKnown && !isCreditPlayer && (logs?.pending_reconcile?.length ?? 0) > 0 ? (

{t("wallet.pendingSectionTitle", { defaultValue: "待对账划转" })}

{t("wallet.pendingSectionHint", { defaultValue: "以下划转仍在与主站对账,请稍后刷新查看结果。", })}

    {logs?.pending_reconcile.map((item) => (
  • {item.direction === "in" ? t("wallet.flow.transfer_in", { defaultValue: "转入" }) : t("wallet.flow.transfer_out", { defaultValue: "转出" })} {formatMinorAsCurrency(item.amount, item.currency_code || currency)}

    {item.transfer_no}

  • ))}
) : null}
{fundingModeKnown ? ( ) : ( )}
); }