diff --git a/package-lock.json b/package-lock.json index 952f187..72665f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "react-dom": "19.2.4", "react-i18next": "^17.0.7", "sonner": "^2.0.7", + "swr": "^2.4.1", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", "zustand": "^5.0.13" @@ -4382,6 +4383,14 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -9723,6 +9732,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", diff --git a/package.json b/package.json index b52be87..1f6718a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-dom": "19.2.4", "react-i18next": "^17.0.7", "sonner": "^2.0.7", + "swr": "^2.4.1", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", "zustand": "^5.0.13" diff --git a/src/features/hall/hall-wallet-strip.tsx b/src/features/hall/hall-wallet-strip.tsx index d8c0f62..81372f0 100644 --- a/src/features/hall/hall-wallet-strip.tsx +++ b/src/features/hall/hall-wallet-strip.tsx @@ -2,8 +2,9 @@ import { Wallet } from "lucide-react"; import Image from "next/image"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; +import { useSWRConfig } from "swr"; import { getWalletBalance } from "@/api/wallet"; import { Skeleton } from "@/components/ui/skeleton"; @@ -12,76 +13,51 @@ import { 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 { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; import { cn } from "@/lib/utils"; import { useNetworkConnectionStore } from "@/stores/network-connection-store"; -import type { WalletBalanceData } from "@/types/api/wallet-balance"; + +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 [balance, setBalance] = useState(null); - const [loading, setLoading] = useState(true); + const { mutate } = useSWRConfig(); const degradedWalletPollRef = useRef(null); const currency = activeCurrency; + const isDegraded = mode === "polling" || mode === "offline"; - const refresh = useCallback(async () => { - const b = await getWalletBalance({ currency: activeCurrency }); - setBalance(b); - }, [activeCurrency]); + const { data: balance, isLoading: loading } = useApiQuery( + BALANCE_KEY(currency), + () => getWalletBalance({ currency }), + { + // 降级模式下 60 秒自动刷新,SWR 内置定时器代替手动 setInterval + refreshInterval: isDegraded ? 60_000 : undefined, + }, + ); + // 全局事件触发刷新(下注后、币种切换等场景) useEffect(() => { - let cancelled = false; - void (async () => { - setLoading(true); - try { - await refresh(); - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, [refresh]); - - useEffect(() => { - const onRefresh = () => void refresh(); + 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); }; - }, [refresh]); + }, [mutate, currency]); + // 非降级模式时清理遗留计时器(refreshInterval 已由 SWR 管理) useEffect(() => { - if (mode !== "polling" && mode !== "offline") { - if (degradedWalletPollRef.current !== null) { - window.clearInterval(degradedWalletPollRef.current); - degradedWalletPollRef.current = null; - } - return; + if (!isDegraded && degradedWalletPollRef.current !== null) { + window.clearInterval(degradedWalletPollRef.current); + degradedWalletPollRef.current = null; } - - if (degradedWalletPollRef.current !== null) { - return; - } - - degradedWalletPollRef.current = window.setInterval(() => { - void refresh(); - }, 60_000); - - return () => { - if (degradedWalletPollRef.current !== null) { - window.clearInterval(degradedWalletPollRef.current); - degradedWalletPollRef.current = null; - } - }; - }, [mode, refresh]); + }, [isDegraded]); const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0); const isCreditPlayer = @@ -136,7 +112,7 @@ export function HallWalletStrip() { currency={currency} lotteryMinor={availableMinor} mainMinor={mainMinor} - onSuccess={refresh} + onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }} /> { await mutate(BALANCE_KEY(currency)); }} /> )} diff --git a/src/features/wallet/transfer-in-screen.tsx b/src/features/wallet/transfer-in-screen.tsx index 7725629..b2eb936 100644 --- a/src/features/wallet/transfer-in-screen.tsx +++ b/src/features/wallet/transfer-in-screen.tsx @@ -1,51 +1,40 @@ "use client"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect } from "react"; +import { useSWRConfig } from "swr"; import { getWalletBalance } from "@/api/wallet"; import { TransferInPage } from "@/features/wallet/wallet-transfer-forms"; import { Skeleton } from "@/components/ui/skeleton"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; +import { useApiQuery } from "@/hooks/use-api-query"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; -import type { WalletBalanceData } from "@/types/api/wallet-balance"; + +const BALANCE_KEY = (currency: string) => ["wallet/balance", currency]; /** 独立路由 `/wallet/transfer-in` */ export function TransferInScreen() { const router = useRouter(); const { activeCurrency: currency } = useActivePlayerCurrency(); - const [balance, setBalance] = useState(null); - const [loading, setLoading] = useState(true); + const { mutate } = useSWRConfig(); - const load = useCallback(async () => { - const b = await getWalletBalance({ currency }); - setBalance(b); - }, [currency]); + const { data: balance, isLoading: loading } = useApiQuery( + BALANCE_KEY(currency), + () => getWalletBalance({ currency }), + ); + // 币种切换时 SWR key 自动变化,此处仅处理全局事件触发的刷新 useEffect(() => { - let c = false; - void (async () => { - try { - await load(); - } finally { - if (!c) setLoading(false); - } - })(); - return () => { - c = true; - }; - }, [load]); - - useEffect(() => { - const onCurrencyChange = () => void load(); - window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange); - return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange); - }, [load]); + const onRefresh = () => void mutate(BALANCE_KEY(currency)); + window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh); + return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh); + }, [mutate, currency]); const onSuccess = useCallback(async () => { - await load(); + await mutate(BALANCE_KEY(currency)); router.push("/wallet"); - }, [load, router]); + }, [mutate, currency, router]); if (loading && !balance) { return ( diff --git a/src/features/wallet/transfer-out-screen.tsx b/src/features/wallet/transfer-out-screen.tsx index 4f2d804..02c6979 100644 --- a/src/features/wallet/transfer-out-screen.tsx +++ b/src/features/wallet/transfer-out-screen.tsx @@ -1,51 +1,40 @@ "use client"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect } from "react"; +import { useSWRConfig } from "swr"; import { getWalletBalance } from "@/api/wallet"; import { TransferOutPage } from "@/features/wallet/wallet-transfer-forms"; import { Skeleton } from "@/components/ui/skeleton"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; +import { useApiQuery } from "@/hooks/use-api-query"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; -import type { WalletBalanceData } from "@/types/api/wallet-balance"; + +const BALANCE_KEY = (currency: string) => ["wallet/balance", currency]; /** 独立路由 `/wallet/transfer-out` */ export function TransferOutScreen() { const router = useRouter(); const { activeCurrency: currency } = useActivePlayerCurrency(); - const [balance, setBalance] = useState(null); - const [loading, setLoading] = useState(true); + const { mutate } = useSWRConfig(); - const load = useCallback(async () => { - const b = await getWalletBalance({ currency }); - setBalance(b); - }, [currency]); + const { data: balance, isLoading: loading } = useApiQuery( + BALANCE_KEY(currency), + () => getWalletBalance({ currency }), + ); + // 币种切换时 SWR key 自动变化,此处仅处理全局事件触发的刷新 useEffect(() => { - let c = false; - void (async () => { - try { - await load(); - } finally { - if (!c) setLoading(false); - } - })(); - return () => { - c = true; - }; - }, [load]); - - useEffect(() => { - const onCurrencyChange = () => void load(); - window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange); - return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange); - }, [load]); + const onRefresh = () => void mutate(BALANCE_KEY(currency)); + window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh); + return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh); + }, [mutate, currency]); const onSuccess = useCallback(async () => { - await load(); + await mutate(BALANCE_KEY(currency)); router.push("/wallet"); - }, [load, router]); + }, [mutate, currency, router]); if (loading && !balance) { return ( diff --git a/src/features/wallet/wallet-screen.tsx b/src/features/wallet/wallet-screen.tsx index 2765850..e83de70 100644 --- a/src/features/wallet/wallet-screen.tsx +++ b/src/features/wallet/wallet-screen.tsx @@ -66,11 +66,13 @@ export function WalletScreen() { setLogsLoading(true); } try { - const b = await getWalletBalance({ currency }); + // 并行请求余额和日志,避免瀑布式串行等待 + const [b, nextLogs] = await Promise.all([ + getWalletBalance({ currency }), + loadLogs(1, false), + ]); if (cancelled) return; setBalance(b); - const nextLogs = await loadLogs(1, false); - if (cancelled) return; setLogs(nextLogs); } catch (e) { if (!cancelled) { @@ -93,9 +95,12 @@ export function WalletScreen() { setError(null); setLogsLoading(true); try { - const b = await getWalletBalance({ currency }); + // 并行请求余额和日志 + const [b] = await Promise.all([ + getWalletBalance({ currency }), + loadLogs(1, false), + ]); setBalance(b); - await loadLogs(1, false); } catch (e) { setError(formatWalletClientError(e, t)); } finally { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 7d98350..e56c5c5 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -5,3 +5,4 @@ export { useWalletPolling, triggerWalletPollingAfterBet } from "./use-wallet-pol export { useWebSocketManager } from "./use-websocket-manager"; export { usePlayerBalanceWs } from "./use-player-balance-ws"; export { usePlayEffectiveWs } from "./use-play-effective-ws"; +export { useApiQuery } from "./use-api-query"; diff --git a/src/hooks/use-api-query.ts b/src/hooks/use-api-query.ts new file mode 100644 index 0000000..e457bc6 --- /dev/null +++ b/src/hooks/use-api-query.ts @@ -0,0 +1,38 @@ +"use client"; + +import useSWR, { type SWRConfiguration, type SWRResponse } from "swr"; + +/** + * 通用 API 查询 Hook —— 基于 SWR 的薄包装。 + * + * @param key SWR 缓存键。传 `null` 表示暂不请求(条件请求)。 + * @param fetcher 无参异步函数,通常由 `api/*.ts` 的函数包装而来。 + * @param options SWR 配置覆盖。 + * + * @example + * ```tsx + * // 简单用法 + * const { data, error, isLoading } = useApiQuery( + * currency ? ["wallet/balance", currency] : null, + * () => getWalletBalance({ currency }), + * ); + * + * // 带自定义刷新间隔 + * const { data } = useApiQuery( + * "draw/current", + * () => getCurrentDraw(), + * { refreshInterval: 5_000 }, + * ); + * ``` + */ +export function useApiQuery( + key: string | readonly unknown[] | null, + fetcher: () => Promise, + options?: SWRConfiguration, +): SWRResponse { + return useSWR(key, fetcher, { + revalidateOnFocus: false, + dedupingInterval: 2_000, + ...options, + }); +} diff --git a/src/hooks/use-wallet-polling.ts b/src/hooks/use-wallet-polling.ts index bf4db9c..b8dae0d 100644 --- a/src/hooks/use-wallet-polling.ts +++ b/src/hooks/use-wallet-polling.ts @@ -2,13 +2,13 @@ import { useCallback, useEffect, useRef } from "react"; -import { getWalletBalance } from "@/api/wallet"; -import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency"; -import { startWalletRefreshBurst } from "@/lib/wallet-refresh-burst"; import { useNetworkConnectionStore } from "@/stores/network-connection-store"; - -const POLLING_INTERVAL_MS = 30_000; // 30秒轮询间隔 -const LIMITED_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询 +import { + refreshWalletNow, + startLimitedWalletPolling, + startPersistentWalletPolling, + stopWalletPolling, +} from "@/lib/wallet-polling-utils"; export type UseWalletPollingReturn = { /** 开始钱包轮询(持续或限时) */ @@ -29,14 +29,10 @@ export type UseWalletPollingReturn = { * 3. 开奖结果后的限时轮询(2分钟) */ export function useWalletPolling(): UseWalletPollingReturn { - const store = useNetworkConnectionStore(); - const { - walletPollingIntervalId, - walletPollingExpiryAt, - setWalletPollingIntervalId, - setWalletPollingExpiryAt, - clearWalletPolling, - } = store; + // 使用 selector 避免整 store 订阅导致无谓重渲染 + const walletPollingIntervalId = useNetworkConnectionStore( + (s) => s.walletPollingIntervalId, + ); const intervalIdRef = useRef(walletPollingIntervalId); @@ -45,79 +41,28 @@ export function useWalletPolling(): UseWalletPollingReturn { intervalIdRef.current = walletPollingIntervalId; }, [walletPollingIntervalId]); - // 刷新钱包余额 - const refreshWallet = useCallback(async () => { - try { - await getWalletBalance({ currency: getActivePlayerCurrencyFromStore() }); - // 触发全局刷新事件,让所有监听组件更新 - window.dispatchEvent(new Event("lottery-wallet-refresh")); - } catch { - // 静默处理错误,避免频繁报错 - } - }, []); - - // 开始钱包轮询 + // 开始钱包轮询(委托给共享工具函数) const startWalletPolling = useCallback( (options?: { limitedDuration?: boolean }) => { const { limitedDuration = false } = options ?? {}; - // 先停止现有的轮询 - if (intervalIdRef.current) { - window.clearInterval(intervalIdRef.current); - intervalIdRef.current = null; - } - - // 立即执行一次刷新 - void refreshWallet(); - - // 设置轮询间隔 - const intervalId = window.setInterval(() => { - // 检查限时轮询是否过期 - if (limitedDuration && walletPollingExpiryAt) { - if (Date.now() > walletPollingExpiryAt) { - // 过期,停止轮询 - clearWalletPolling(); - intervalIdRef.current = null; - return; - } - } - void refreshWallet(); - }, POLLING_INTERVAL_MS); - - intervalIdRef.current = intervalId; - setWalletPollingIntervalId(intervalId); - - // 设置限时轮询的过期时间 if (limitedDuration) { - const expiryAt = Date.now() + LIMITED_POLLING_DURATION_MS; - setWalletPollingExpiryAt(expiryAt); - - // 设置一个定时器在过期时清理 - window.setTimeout(() => { - if (intervalIdRef.current === intervalId) { - clearWalletPolling(); - intervalIdRef.current = null; - } - }, LIMITED_POLLING_DURATION_MS + 1000); // 稍微延迟确保interval先执行检查 + startLimitedWalletPolling(); + } else { + startPersistentWalletPolling(); } }, - [ - walletPollingExpiryAt, - refreshWallet, - setWalletPollingIntervalId, - setWalletPollingExpiryAt, - clearWalletPolling, - ], + [], ); // 停止钱包轮询 - const stopWalletPolling = useCallback(() => { + const handleStop = useCallback(() => { if (intervalIdRef.current) { window.clearInterval(intervalIdRef.current); intervalIdRef.current = null; } - clearWalletPolling(); - }, [clearWalletPolling]); + stopWalletPolling(); + }, []); // 组件卸载时清理 useEffect(() => { @@ -130,8 +75,8 @@ export function useWalletPolling(): UseWalletPollingReturn { return { startWalletPolling, - stopWalletPolling, - refreshWallet, + stopWalletPolling: handleStop, + refreshWallet: refreshWalletNow, isPolling: walletPollingIntervalId !== null, }; } @@ -144,7 +89,7 @@ export function triggerWalletPollingAfterBet(): void { const store = useNetworkConnectionStore.getState(); if (store.mode === "polling" || store.mode === "offline") { - startWalletRefreshBurst(); + startLimitedWalletPolling(); return; } diff --git a/src/hooks/use-websocket-manager.ts b/src/hooks/use-websocket-manager.ts index 85e48b7..c707c5f 100644 --- a/src/hooks/use-websocket-manager.ts +++ b/src/hooks/use-websocket-manager.ts @@ -2,20 +2,22 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { getWalletBalance } from "@/api/wallet"; -import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency"; import { disconnectLotteryEcho, getLotteryEcho, isLotteryReverbConfigured, } from "@/lib/lottery-echo"; +import { + refreshWalletNow, + startLimitedWalletPolling, + startPersistentWalletPolling, + stopWalletPolling as stopWalletPollingUtil, +} from "@/lib/wallet-polling-utils"; import { useNetworkConnectionStore, type NetworkMode, } from "@/stores/network-connection-store"; -const POLLING_INTERVAL_MS = 30_000; // 30秒轮询间隔 -const WALLET_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询 const RECONNECT_INTERVAL_MS = 5_000; // 5秒重连间隔 const MAX_RECONNECT_ATTEMPTS = 10; // 最大重连次数 @@ -59,81 +61,45 @@ export type UseWebSocketManagerReturn = { * 5. 可选钱包余额轮询(`startWalletPolling`,下注等场景) */ export function useWebSocketManager(): UseWebSocketManagerReturn { - const store = useNetworkConnectionStore(); + // 使用 selector 避免整 store 订阅导致无谓重渲染 + const mode = useNetworkConnectionStore((s) => s.mode); + const reconnectAttempts = useNetworkConnectionStore((s) => s.reconnectAttempts); + const setWebSocketConnected = useNetworkConnectionStore((s) => s.setWebSocketConnected); + const setReconnecting = useNetworkConnectionStore((s) => s.setReconnecting); + const incrementReconnectAttempts = useNetworkConnectionStore((s) => s.incrementReconnectAttempts); + const resetReconnectAttempts = useNetworkConnectionStore((s) => s.resetReconnectAttempts); + const setLastDisconnectedAt = useNetworkConnectionStore((s) => s.setLastDisconnectedAt); + const switchToPollingMode = useNetworkConnectionStore((s) => s.switchToPollingMode); + const switchToWebSocketMode = useNetworkConnectionStore((s) => s.switchToWebSocketMode); + const reconnectTimerRef = useRef(null); const attemptReconnectRef = useRef<() => void>(() => {}); /** 递增后重新创建 Echo 并绑定连接事件(供「恢复」与重连使用) */ const [echoSession, setEchoSession] = useState(0); - const { - mode, - reconnectAttempts, - setWebSocketConnected, - setReconnecting, - incrementReconnectAttempts, - resetReconnectAttempts, - setLastDisconnectedAt, - switchToPollingMode, - switchToWebSocketMode, - clearWalletPolling, - } = store; - // 刷新画作数据 const refreshDraw = useCallback(async () => { // 由 useHallDrawLive 监听并拉取,避免与大厅重复请求 draw/current window.dispatchEvent(new Event("lottery-hall-refresh")); }, []); - // 刷新钱包余额 - const refreshWallet = useCallback(async () => { - try { - await getWalletBalance({ currency: getActivePlayerCurrencyFromStore() }); - // 触发全局刷新事件,让组件更新 - window.dispatchEvent(new Event("lottery-wallet-refresh")); - } catch { - // 静默处理错误 - } - }, []); - - // 钱包轮询 + // 钱包轮询(委托给共享工具函数,消除与 useWalletPolling 的重复逻辑) const startWalletPolling = useCallback( (options?: { limitedDuration?: boolean }) => { const { limitedDuration = false } = options ?? {}; - const s0 = useNetworkConnectionStore.getState(); - - const prevWalletId = s0.walletPollingIntervalId; - if (prevWalletId !== null) { - window.clearInterval(prevWalletId); - } - - void refreshWallet(); - - const intervalId = window.setInterval(() => { - const s = useNetworkConnectionStore.getState(); - if ( - limitedDuration && - s.walletPollingExpiryAt !== null && - Date.now() > s.walletPollingExpiryAt - ) { - s.clearWalletPolling(); - return; - } - void refreshWallet(); - }, POLLING_INTERVAL_MS); - - s0.setWalletPollingIntervalId(intervalId); - if (limitedDuration) { - s0.setWalletPollingExpiryAt(Date.now() + WALLET_POLLING_DURATION_MS); + startLimitedWalletPolling(); + } else { + startPersistentWalletPolling(); } }, - [refreshWallet], + [], ); // 停止钱包轮询 const stopWalletPolling = useCallback(() => { - clearWalletPolling(); - }, [clearWalletPolling]); + stopWalletPollingUtil(); + }, []); // 检测 Pusher 是否已真正 connected(非仅订阅频道) const isPusherConnected = useCallback((): boolean => { @@ -235,7 +201,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { const handleOffline = () => { // 网络断开 - store.setMode("offline"); + useNetworkConnectionStore.getState().setMode("offline"); }; window.addEventListener("online", handleOnline); @@ -245,7 +211,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; - }, [mode, reconnect, store]); + }, [mode, reconnect]); // 初始化:绑定 Pusher 真实连接事件(勿仅凭 channel 对象存在判定已连接) useEffect(() => { @@ -349,6 +315,6 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { startWalletPolling, stopWalletPolling, refreshDraw, - refreshWallet, + refreshWallet: refreshWalletNow, }; } diff --git a/src/lib/wallet-polling-utils.ts b/src/lib/wallet-polling-utils.ts new file mode 100644 index 0000000..a0c4189 --- /dev/null +++ b/src/lib/wallet-polling-utils.ts @@ -0,0 +1,70 @@ +import { getWalletBalance } from "@/api/wallet"; +import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency"; +import { useNetworkConnectionStore } from "@/stores/network-connection-store"; + +const POLLING_INTERVAL_MS = 30_000; // 30秒轮询间隔 +const LIMITED_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询 + +/** + * 非 Hook 钱包刷新工具 —— 供 Hook 与非 React 上下文共用。 + * + * 立即拉取一次余额,并触发 `lottery-wallet-refresh` 全局事件。 + */ +export async function refreshWalletNow(): Promise { + try { + await getWalletBalance({ currency: getActivePlayerCurrencyFromStore() }); + window.dispatchEvent(new Event("lottery-wallet-refresh")); + } catch { + // 静默处理错误,避免频繁报错 + } +} + +/** + * 启动钱包限时轮询(2 分钟内每 30s 刷新一次)。 + * 先清除已有的钱包轮询计时器,避免泄漏。 + */ +export function startLimitedWalletPolling(): void { + const store = useNetworkConnectionStore.getState(); + store.clearWalletPolling(); + + void refreshWalletNow(); + + const expiryAt = Date.now() + LIMITED_POLLING_DURATION_MS; + store.setWalletPollingExpiryAt(expiryAt); + + const intervalId = window.setInterval(() => { + const s = useNetworkConnectionStore.getState(); + if ( + s.walletPollingExpiryAt !== null && + Date.now() > s.walletPollingExpiryAt + ) { + s.clearWalletPolling(); + return; + } + void refreshWalletNow(); + }, POLLING_INTERVAL_MS); + + store.setWalletPollingIntervalId(intervalId); +} + +/** + * 启动钱包持续轮询(直到手动停止)。 + * 先清除已有的钱包轮询计时器,避免泄漏。 + */ +export function startPersistentWalletPolling(): void { + const store = useNetworkConnectionStore.getState(); + store.clearWalletPolling(); + + void refreshWalletNow(); + + const intervalId = window.setInterval(() => { + void refreshWalletNow(); + }, POLLING_INTERVAL_MS); + + store.setWalletPollingIntervalId(intervalId); +} + +/** 停止钱包轮询 */ +export function stopWalletPolling(): void { + useNetworkConnectionStore.getState().clearWalletPolling(); +} diff --git a/src/lib/wallet-refresh-burst.ts b/src/lib/wallet-refresh-burst.ts index 7b529e1..867a46c 100644 --- a/src/lib/wallet-refresh-burst.ts +++ b/src/lib/wallet-refresh-burst.ts @@ -1,27 +1,12 @@ -import { useNetworkConnectionStore } from "@/stores/network-connection-store"; - -const WALLET_REFRESH_BURST_MS = 30_000; -const WALLET_REFRESH_BURST_DURATION_MS = 2 * 60 * 1000; +/** + * @deprecated 请直接使用 `@/lib/wallet-polling-utils` 的 `startLimitedWalletPolling`。 + * 此文件保留向后兼容,内部已委托给共享工具函数。 + */ +import { startLimitedWalletPolling } from "@/lib/wallet-polling-utils"; /** * 开奖等场景:立即刷新余额,并在限时内每 30s 派发 `lottery-wallet-refresh`(先清旧定时器,避免泄漏)。 */ export function startWalletRefreshBurst(): void { - const store = useNetworkConnectionStore.getState(); - store.clearWalletPolling(); - - window.dispatchEvent(new Event("lottery-wallet-refresh")); - - const expiryAt = Date.now() + WALLET_REFRESH_BURST_DURATION_MS; - store.setWalletPollingExpiryAt(expiryAt); - - const intervalId = window.setInterval(() => { - if (Date.now() > expiryAt) { - useNetworkConnectionStore.getState().clearWalletPolling(); - return; - } - window.dispatchEvent(new Event("lottery-wallet-refresh")); - }, WALLET_REFRESH_BURST_MS); - - store.setWalletPollingIntervalId(intervalId); + startLimitedWalletPolling(); }