Files
lotteryFront/src/features/hall/hall-wallet-strip.tsx
kang add3d1fc05
Some checks failed
lotteryfront CI / build (push) Has been cancelled
feat: enhance wallet strip for mobile responsiveness
- 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.
2026-07-08 15:57:09 +08:00

172 lines
6.6 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 { 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>
);
}