Files
lotteryFront/src/features/hall/hall-wallet-strip.tsx
kang ecb032f8cc
Some checks failed
lotteryfront CI / build (push) Has been cancelled
feat: enhance API error handling and UI improvements
- Updated `getPublicCurrencies` and `getPlayerAuthCaptcha` functions to include `skipGlobalServerError` option, improving error handling for API requests.
- Refactored the NotFoundPage component to enhance mobile responsiveness and UI consistency, integrating `PlayerMobileViewport`.
- Improved the NetworkStatusBanner and OfflineBanner components by adding player banner height reference for better layout management.
- Updated Wallet components to utilize `PlayerMoneyDisplay` for consistent currency representation and added credit mode handling in various screens.
- Enhanced the WalletScreen and Transfer screens with loading panels and credit guard logic for better user experience during wallet operations.
2026-06-22 10:50:52 +08:00

153 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { Wallet } from "lucide-react";
import Image from "next/image";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useSWRConfig } from "swr";
import { getWalletBalance } from "@/api/wallet";
import { PlayerMoneyDisplay } from "@/components/player-money-display";
import { Skeleton } from "@/components/ui/skeleton";
import {
TransferInDialog,
TransferOutDialog,
} from "@/features/wallet/wallet-transfer-dialogs";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { useApiQuery } from "@/hooks/use-api-query";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { cn } from "@/lib/utils";
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
export function HallWalletStrip() {
const mode = useNetworkConnectionStore((s) => s.mode);
const { t } = useTranslation("player");
const { activeCurrency } = useActivePlayerCurrency();
const { mutate } = useSWRConfig();
const degradedWalletPollRef = useRef<number | null>(null);
const currency = activeCurrency;
const isDegraded = mode === "polling" || mode === "offline";
const { data: balance, isLoading: loading } = useApiQuery(
BALANCE_KEY(currency),
() => getWalletBalance({ currency }),
{
// 降级模式下 60 秒自动刷新SWR 内置定时器代替手动 setInterval
refreshInterval: isDegraded ? 60_000 : undefined,
},
);
// 全局事件触发刷新(下注后、币种切换等场景)
useEffect(() => {
const onRefresh = () => void mutate(BALANCE_KEY(currency));
window.addEventListener("lottery-wallet-refresh", onRefresh);
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
return () => {
window.removeEventListener("lottery-wallet-refresh", onRefresh);
window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
};
}, [mutate, currency]);
// 非降级模式时清理遗留计时器refreshInterval 已由 SWR 管理)
useEffect(() => {
if (!isDegraded && degradedWalletPollRef.current !== null) {
window.clearInterval(degradedWalletPollRef.current);
degradedWalletPollRef.current = null;
}
}, [isDegraded]);
const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0);
const isCreditPlayer = isCreditFundingPlayer(balance);
const balanceMinor = Number(balance?.balance ?? 0);
const headlineMinor = isCreditPlayer ? availableMinor : balanceMinor;
const transferInLotteryMinor = isCreditPlayer ? availableMinor : balanceMinor;
const mainMinor =
balance?.main_balance === null || balance?.main_balance === undefined
? null
: Number(balance.main_balance);
return (
<section
className="mb-3 space-y-2.5"
aria-label={
isCreditPlayer
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
: t("wallet.balance")
}
>
<div
className={cn(
"relative overflow-hidden rounded-xl bg-[#e5002c] px-3 py-3 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
)}
>
<Image
src="/entry/image5.png"
alt=""
fill
className="pointer-events-none object-cover object-center"
aria-hidden
/>
<div className="relative flex items-center gap-3">
<div className="flex size-14 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
<Wallet className="size-7" aria-hidden />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-white/90">
{isCreditPlayer
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
: t("wallet.balance")}
</p>
{loading ? (
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
) : (
<PlayerMoneyDisplay
amountMinor={headlineMinor}
currency={currency}
className="mt-1 text-white"
/>
)}
{isCreditPlayer && !loading && balance ? (
<p className="mt-2 text-xs text-white/75">
{t("wallet.creditSummary", {
defaultValue: "授信 {{limit}} · 已用 {{used}}",
limit: formatMinorAsCurrency(balance.credit_limit ?? 0, currency),
used: formatMinorAsCurrency(balance.used_credit ?? 0, currency),
})}
</p>
) : null}
</div>
</div>
</div>
{isCreditPlayer ? null : (
<div className="grid grid-cols-2 gap-2.5">
<TransferInDialog
idPrefix="hall-"
triggerVariant="hall"
triggerLabel={t("wallet.transferIn")}
triggerClassName="h-12 rounded-lg text-base font-bold"
currency={currency}
lotteryMinor={transferInLotteryMinor}
mainMinor={mainMinor}
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}
/>
<TransferOutDialog
idPrefix="hall-"
triggerVariant="hall"
triggerLabel={t("wallet.transferOut")}
triggerClassName="h-12 rounded-lg text-base font-bold"
currency={currency}
availableMinor={availableMinor}
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}
/>
</div>
)}
</section>
);
}