Some checks failed
lotteryfront CI / build (push) Has been cancelled
- Added mobile-specific styles to the HallWalletStrip component for better usability on smaller screens. - Adjusted padding, font sizes, and spacing based on device type. - Introduced a new hook `useIsMobile` to determine the device type. feat: sort ticket items by provider code - Updated the sorting logic in `sortTicketGroupItems` to include provider code for better organization of ticket items. feat: display provider information in ticket order details - Added a `providerLabel` function to format provider information. - Updated `TicketOrderDetailScreen` and `TicketOrdersListScreen` to display provider name and code. feat: enhance ticket item types with provider information - Extended `TicketItemListRow` and `TicketItemDetailPayload` types to include optional provider code and name. feat: add API for fetching bet providers - Created a new API endpoint to retrieve bet providers. - Defined types for bet provider data in `bet-provider.ts`. chore: update player spacing constants - Adjusted padding values in `player-spacing.ts` for improved layout consistency.
172 lines
6.6 KiB
TypeScript
172 lines
6.6 KiB
TypeScript
"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 { useIsMobile } from "@/hooks/use-mobile";
|
||
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";
|
||
import { usePlayerSessionStore } from "@/stores/player-session-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 isMobile = useIsMobile();
|
||
|
||
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 profile = usePlayerSessionStore((s) => s.profile);
|
||
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
|
||
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={cn("mb-3", isMobile ? "space-y-2" : "space-y-2.5")}
|
||
aria-label={
|
||
isCreditPlayer
|
||
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
|
||
: t("wallet.balance")
|
||
}
|
||
>
|
||
<div
|
||
className={cn(
|
||
"relative overflow-hidden rounded-xl bg-[#e5002c] text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
|
||
isMobile ? "px-3 py-2.5" : "px-3 py-3",
|
||
)}
|
||
>
|
||
<Image
|
||
src="/entry/image5.png"
|
||
alt=""
|
||
fill
|
||
sizes="(max-width: 768px) 100vw, 768px"
|
||
className="pointer-events-none object-cover object-center"
|
||
aria-hidden
|
||
/>
|
||
<div className={cn("relative flex items-center", isMobile ? "gap-2.5" : "gap-3")}>
|
||
<div
|
||
className={cn(
|
||
"shrink-0 rounded-full bg-white text-[#d81435] shadow-sm",
|
||
isMobile
|
||
? "flex size-11 items-center justify-center"
|
||
: "flex size-14 items-center justify-center",
|
||
)}
|
||
>
|
||
<Wallet className={cn(isMobile ? "size-5.5" : "size-7")} aria-hidden />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<p className={cn("font-semibold text-white/90", isMobile ? "text-[13px]" : "text-sm")}>
|
||
{isCreditPlayer
|
||
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
|
||
: t("wallet.balance")}
|
||
</p>
|
||
{loading ? (
|
||
<Skeleton className={cn("rounded-md bg-white/25", isMobile ? "mt-1.5 h-7 w-36" : "mt-2 h-8 w-44")} />
|
||
) : (
|
||
<PlayerMoneyDisplay
|
||
amountMinor={headlineMinor}
|
||
currency={currency}
|
||
className={cn("text-white", isMobile ? "mt-0.5" : "mt-1")}
|
||
/>
|
||
)}
|
||
{isCreditPlayer && !loading && balance ? (
|
||
<p className={cn("text-white/75", isMobile ? "mt-1 text-[11px]" : "mt-2 text-xs")}>
|
||
{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={cn("grid grid-cols-2", isMobile ? "gap-2" : "gap-2.5")}>
|
||
<TransferInDialog
|
||
idPrefix="hall-"
|
||
triggerVariant="hall"
|
||
triggerLabel={t("wallet.transferIn")}
|
||
triggerClassName={cn(
|
||
"rounded-lg font-bold",
|
||
isMobile ? "h-10 text-sm" : "h-12 text-base",
|
||
)}
|
||
currency={currency}
|
||
lotteryMinor={transferInLotteryMinor}
|
||
mainMinor={mainMinor}
|
||
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}
|
||
/>
|
||
<TransferOutDialog
|
||
idPrefix="hall-"
|
||
triggerVariant="hall"
|
||
triggerLabel={t("wallet.transferOut")}
|
||
triggerClassName={cn(
|
||
"rounded-lg font-bold",
|
||
isMobile ? "h-10 text-sm" : "h-12 text-base",
|
||
)}
|
||
currency={currency}
|
||
availableMinor={availableMinor}
|
||
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}
|
||
/>
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|