feat: 集成网络连接管理与降级轮询功能
- 在 PlayerAppShell 中引入 NetworkStatusBanner 组件以显示网络状态 - 在 HallBettingGrid 中实现下注后触发钱包轮询 - 在 HallWalletStrip 中添加网络连接状态管理与定期刷新逻辑 - 在 useHallDrawLive 中集成 WebSocket 连接状态与降级轮询机制,确保在断开时自动切换到轮询模式
This commit is contained in:
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getDrawCurrent } from "@/api/draw";
|
||||
import { getLotteryEcho } from "@/lib/lottery-echo";
|
||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
||||
import type { DrawCurrentPayload } from "@/types/api/draw-current";
|
||||
|
||||
/** 界面文档 §2.1:`draw.countdown` / `draw.status_change` / `result.published` 载荷 */
|
||||
@@ -34,6 +35,7 @@ function applySnapshotDrift(
|
||||
|
||||
/**
|
||||
* 大厅期号:WebSocket `lottery-hall` + 轮询降级(与 {@link HallDrawPanel} 同源逻辑)。
|
||||
* 已集成网络连接管理,WebSocket断开时自动切换到轮询模式。
|
||||
*/
|
||||
export function useHallDrawLive(): {
|
||||
raw: DrawCurrentPayload | null | undefined;
|
||||
@@ -47,6 +49,21 @@ export function useHallDrawLive(): {
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 网络连接状态
|
||||
const mode = useNetworkConnectionStore((s) => s.mode);
|
||||
const isWebSocketConnected = useNetworkConnectionStore(
|
||||
(s) => s.isWebSocketConnected,
|
||||
);
|
||||
const setDrawPollingIntervalId = useNetworkConnectionStore(
|
||||
(s) => s.setDrawPollingIntervalId,
|
||||
);
|
||||
const setWalletPollingIntervalId = useNetworkConnectionStore(
|
||||
(s) => s.setWalletPollingIntervalId,
|
||||
);
|
||||
const setWalletPollingExpiryAt = useNetworkConnectionStore(
|
||||
(s) => s.setWalletPollingExpiryAt,
|
||||
);
|
||||
|
||||
const mergeFromWs = useCallback((evt: HallWsEnvelope) => {
|
||||
setRaw(evt.data);
|
||||
setEmittedAtMs(evt.emitted_at_ms ?? Date.now());
|
||||
@@ -64,11 +81,13 @@ export function useHallDrawLive(): {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// WebSocket 正常时的轮询间隔(作为备用)
|
||||
const refreshMs = useMemo(() => {
|
||||
if (raw === undefined) return 10_000;
|
||||
return raw ? 30_000 : 12_000;
|
||||
if (raw === undefined) return 30_000;
|
||||
return raw ? 60_000 : 30_000; // WebSocket正常时减少轮询频率
|
||||
}, [raw]);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
void load();
|
||||
@@ -76,13 +95,7 @@ export function useHallDrawLive(): {
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => {
|
||||
void load();
|
||||
}, refreshMs);
|
||||
return () => window.clearInterval(id);
|
||||
}, [load, refreshMs]);
|
||||
|
||||
// 本地倒计时计时器(用于 UI 更新)
|
||||
useEffect(() => {
|
||||
const bump = () => setNowMs(Date.now());
|
||||
bump();
|
||||
@@ -97,20 +110,113 @@ export function useHallDrawLive(): {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// WebSocket 订阅 + 降级轮询逻辑
|
||||
useEffect(() => {
|
||||
const echo = getLotteryEcho();
|
||||
if (!echo) return;
|
||||
|
||||
echo
|
||||
.channel("lottery-hall")
|
||||
.listen(".draw.countdown", mergeFromWs)
|
||||
.listen(".draw.status_change", mergeFromWs)
|
||||
.listen(".result.published", mergeFromWs);
|
||||
// 监听 WebSocket 事件
|
||||
const channel = echo.channel("lottery-hall");
|
||||
|
||||
// 设置事件监听
|
||||
channel.listen(".draw.countdown", mergeFromWs);
|
||||
channel.listen(".draw.status_change", mergeFromWs);
|
||||
channel.listen(".result.published", (evt: HallWsEnvelope) => {
|
||||
// 开奖结果发布时触发钱包轮询
|
||||
mergeFromWs(evt);
|
||||
|
||||
// 触发钱包余额轮询(开奖后需要更新余额)
|
||||
const intervalId = window.setInterval(() => {
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
}, 30_000);
|
||||
setWalletPollingIntervalId(intervalId);
|
||||
// 设置2分钟后停止轮询
|
||||
setWalletPollingExpiryAt(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
// 2分钟后自动清理
|
||||
window.setTimeout(() => {
|
||||
window.clearInterval(intervalId);
|
||||
setWalletPollingIntervalId(null);
|
||||
}, 2 * 60 * 1000);
|
||||
});
|
||||
|
||||
// 连接状态变化处理
|
||||
const handleConnected = () => {
|
||||
// WebSocket 连接成功,停止降级轮询
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (currentPollingId) {
|
||||
window.clearInterval(currentPollingId);
|
||||
setDrawPollingIntervalId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnected = () => {
|
||||
// WebSocket 断开,启动降级轮询(如果还没启动)
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (!currentPollingId) {
|
||||
const intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, 30_000); // WebSocket断开时使用30秒轮询
|
||||
setDrawPollingIntervalId(intervalId);
|
||||
}
|
||||
};
|
||||
|
||||
// 订阅 Echo 的连接事件(如果支持)
|
||||
if (echo.connector?.pusher) {
|
||||
echo.connector.pusher.connection.bind("connected", handleConnected);
|
||||
echo.connector.pusher.connection.bind("disconnected", handleDisconnected);
|
||||
}
|
||||
|
||||
// 如果当前是轮询模式,启动轮询
|
||||
if (mode === "polling" || mode === "offline") {
|
||||
handleDisconnected();
|
||||
}
|
||||
|
||||
return () => {
|
||||
echo.leave("lottery-hall");
|
||||
if (echo.connector?.pusher) {
|
||||
echo.connector.pusher.connection.unbind("connected", handleConnected);
|
||||
echo.connector.pusher.connection.unbind(
|
||||
"disconnected",
|
||||
handleDisconnected,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [mergeFromWs]);
|
||||
}, [
|
||||
mergeFromWs,
|
||||
load,
|
||||
setDrawPollingIntervalId,
|
||||
setWalletPollingIntervalId,
|
||||
setWalletPollingExpiryAt,
|
||||
mode,
|
||||
]);
|
||||
|
||||
// WebSocket 断开时的兜底轮询(独立于 Echo 事件监听)
|
||||
useEffect(() => {
|
||||
let intervalId: number | null = null;
|
||||
|
||||
// 只在 WebSocket 未连接且没有轮询计时器时启动轮询
|
||||
if (!isWebSocketConnected && mode !== "websocket") {
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (!currentPollingId) {
|
||||
// 立即执行一次
|
||||
void load();
|
||||
|
||||
// 设置30秒轮询
|
||||
intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, 30_000);
|
||||
setDrawPollingIntervalId(intervalId);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId);
|
||||
setDrawPollingIntervalId(null);
|
||||
}
|
||||
};
|
||||
}, [isWebSocketConnected, mode, load, setDrawPollingIntervalId]);
|
||||
|
||||
const display: DrawCurrentPayload | null | undefined =
|
||||
raw === undefined || raw === null ? raw : applySnapshotDrift(raw, emittedAtMs, nowMs);
|
||||
|
||||
Reference in New Issue
Block a user