feat: 优化大厅期号倒计时并改用服务端时间同步
This commit is contained in:
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user