feat: 优化大厅期号倒计时并改用服务端时间同步

This commit is contained in:
2026-05-16 10:32:05 +08:00
parent d5415888e6
commit 9a1dea59de
4 changed files with 43 additions and 14 deletions

View File

@@ -1,14 +1,14 @@
import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { DrawCurrentPayload } from "@/types/api/draw-current";
import type { DrawCurrentResponse } from "@/types/api/draw-current";
import type {
DrawResultDetailPayload,
DrawResultsListPayload,
} from "@/types/api/draw-results";
/** `GET /api/v1/draw/current`(无需登录;无当前期时 `data` 为 `null` */
export function getDrawCurrent(): Promise<DrawCurrentPayload | null> {
return lotteryRequest.get<DrawCurrentPayload | null>(
export function getDrawCurrent(): Promise<DrawCurrentResponse> {
return lotteryRequest.get<DrawCurrentResponse>(
`${API_V1_PREFIX}/draw/current`,
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { Hourglass, Landmark, TimerReset } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@@ -38,24 +39,40 @@ function CurrentTime({ payload }: { payload: DrawCurrentPayload }) {
}
function CloseTime({
serverNowMs,
hud,
payload,
}: {
serverNowMs: number;
hud: ReturnType<typeof drawStatusHud>;
payload: DrawCurrentPayload;
}) {
const { t } = useTranslation("player");
const sealedCountdown = isHallSealedCountdownUi(payload.status);
const [nowMs, setNowMs] = useState(serverNowMs);
useEffect(() => {
setNowMs(serverNowMs);
}, [serverNowMs]);
useEffect(() => {
const intervalId = window.setInterval(() => {
setNowMs((current) => current + 1000);
}, 1000);
return () => window.clearInterval(intervalId);
}, []);
let seconds = 0;
let label = t("draw.closesIn");
if (hud.countdownKind === "close") {
seconds = Math.max(0, payload.seconds_to_close);
seconds = Math.max(0, Math.ceil(((payload.close_time ? Date.parse(payload.close_time) : 0) - nowMs) / 1000));
} else if (hud.countdownKind === "draw") {
seconds = Math.max(0, payload.seconds_to_draw);
seconds = Math.max(0, Math.ceil(((payload.draw_time ? Date.parse(payload.draw_time) : 0) - nowMs) / 1000));
label = sealedCountdown ? t("draw.drawsIn") : t("draw.closesIn");
} else if (hud.countdownKind === "cooldown") {
seconds = Math.max(0, payload.seconds_remaining_in_cooldown ?? 0);
seconds = Math.max(0, Math.ceil(((payload.cooling_end_time ? Date.parse(payload.cooling_end_time) : 0) - nowMs) / 1000));
label = t("draw.coolDown");
}
@@ -70,7 +87,7 @@ function CloseTime({
}
export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {
const { raw, display, error, reload } = drawLive;
const { raw, display, serverNowMs, error, reload } = drawLive;
const { t } = useTranslation("player");
if (error) {
@@ -112,7 +129,6 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
const hud = drawStatusHud(display.status);
const sealedUi = isHallSealedCountdownUi(display.status);
return (
<section
className={cn(
@@ -134,7 +150,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
<CurrentTime payload={display} />
</div>
<div className="relative flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
<CloseTime hud={hud} payload={display} />
<CloseTime serverNowMs={serverNowMs} hud={hud} payload={display} />
<Hourglass
className={cn(
"absolute right-2 top-1/2 size-5 -translate-y-1/2",

View File

@@ -5,12 +5,13 @@ import { useCallback, useEffect, 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";
import type { DrawCurrentPayload, DrawCurrentResponse } from "@/types/api/draw-current";
/** 大厅共享的当期快照(由 {@link useHallDrawLive} 产出,供期号条与下注表共用)。 */
export type HallDrawLiveSnapshot = {
raw: DrawCurrentPayload | null | undefined;
display: DrawCurrentPayload | null | undefined;
serverNowMs: number;
error: string | null;
reload: () => Promise<void>;
isBettable: boolean;
@@ -48,6 +49,7 @@ function applySnapshotDrift(
*/
export function useHallDrawLive(): HallDrawLiveSnapshot {
const [raw, setRaw] = useState<DrawCurrentPayload | null | undefined>(undefined);
const [serverNowMs, setServerNowMs] = useState(() => Date.now());
const [emittedAtMs, setEmittedAtMs] = useState(() => Date.now());
const [nowMs, setNowMs] = useState(() => Date.now());
const [error, setError] = useState<string | null>(null);
@@ -72,17 +74,22 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
setEmittedAtMs(evt.emitted_at_ms ?? Date.now());
}, []);
const updateFromResponse = useCallback((resp: DrawCurrentResponse) => {
setServerNowMs(resp.server_now_ms);
setRaw(resp.data);
setEmittedAtMs(resp.server_now_ms);
}, []);
const load = useCallback(async () => {
try {
setError(null);
const d = await getDrawCurrent();
setRaw(d);
setEmittedAtMs(Date.now());
updateFromResponse(d);
} catch {
setError("draw.loadFailedRefresh");
setRaw(undefined);
}
}, []);
}, [updateFromResponse]);
// 初始加载
useEffect(() => {
@@ -228,5 +235,5 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
const isBettable = display != null && display.status === "open";
return { raw, display, error, reload: load, isBettable };
return { raw, display, serverNowMs, error, reload: load, isBettable };
}

View File

@@ -36,3 +36,9 @@ export type DrawCurrentPayload = {
result_version?: number;
result_source?: string | null;
};
/** `GET /api/v1/draw/current` 的完整响应 */
export type DrawCurrentResponse = {
server_now_ms: number;
data: DrawCurrentPayload | null;
};