feat: 优化大厅期号倒计时并改用服务端时间同步
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import { lotteryRequest } from "@/lib/lottery-http";
|
import { lotteryRequest } from "@/lib/lottery-http";
|
||||||
import { API_V1_PREFIX } from "@/api/paths";
|
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 {
|
import type {
|
||||||
DrawResultDetailPayload,
|
DrawResultDetailPayload,
|
||||||
DrawResultsListPayload,
|
DrawResultsListPayload,
|
||||||
} from "@/types/api/draw-results";
|
} from "@/types/api/draw-results";
|
||||||
|
|
||||||
/** `GET /api/v1/draw/current`(无需登录;无当前期时 `data` 为 `null`) */
|
/** `GET /api/v1/draw/current`(无需登录;无当前期时 `data` 为 `null`) */
|
||||||
export function getDrawCurrent(): Promise<DrawCurrentPayload | null> {
|
export function getDrawCurrent(): Promise<DrawCurrentResponse> {
|
||||||
return lotteryRequest.get<DrawCurrentPayload | null>(
|
return lotteryRequest.get<DrawCurrentResponse>(
|
||||||
`${API_V1_PREFIX}/draw/current`,
|
`${API_V1_PREFIX}/draw/current`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Hourglass, Landmark, TimerReset } from "lucide-react";
|
import { Hourglass, Landmark, TimerReset } from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -38,24 +39,40 @@ function CurrentTime({ payload }: { payload: DrawCurrentPayload }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CloseTime({
|
function CloseTime({
|
||||||
|
serverNowMs,
|
||||||
hud,
|
hud,
|
||||||
payload,
|
payload,
|
||||||
}: {
|
}: {
|
||||||
|
serverNowMs: number;
|
||||||
hud: ReturnType<typeof drawStatusHud>;
|
hud: ReturnType<typeof drawStatusHud>;
|
||||||
payload: DrawCurrentPayload;
|
payload: DrawCurrentPayload;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
const sealedCountdown = isHallSealedCountdownUi(payload.status);
|
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 seconds = 0;
|
||||||
let label = t("draw.closesIn");
|
let label = t("draw.closesIn");
|
||||||
|
|
||||||
if (hud.countdownKind === "close") {
|
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") {
|
} 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");
|
label = sealedCountdown ? t("draw.drawsIn") : t("draw.closesIn");
|
||||||
} else if (hud.countdownKind === "cooldown") {
|
} 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");
|
label = t("draw.coolDown");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +87,7 @@ function CloseTime({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {
|
export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {
|
||||||
const { raw, display, error, reload } = drawLive;
|
const { raw, display, serverNowMs, error, reload } = drawLive;
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -112,7 +129,6 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
|
|||||||
|
|
||||||
const hud = drawStatusHud(display.status);
|
const hud = drawStatusHud(display.status);
|
||||||
const sealedUi = isHallSealedCountdownUi(display.status);
|
const sealedUi = isHallSealedCountdownUi(display.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -134,7 +150,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
|
|||||||
<CurrentTime payload={display} />
|
<CurrentTime payload={display} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
|
<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
|
<Hourglass
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-2 top-1/2 size-5 -translate-y-1/2",
|
"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 { getDrawCurrent } from "@/api/draw";
|
||||||
import { getLotteryEcho } from "@/lib/lottery-echo";
|
import { getLotteryEcho } from "@/lib/lottery-echo";
|
||||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
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} 产出,供期号条与下注表共用)。 */
|
/** 大厅共享的当期快照(由 {@link useHallDrawLive} 产出,供期号条与下注表共用)。 */
|
||||||
export type HallDrawLiveSnapshot = {
|
export type HallDrawLiveSnapshot = {
|
||||||
raw: DrawCurrentPayload | null | undefined;
|
raw: DrawCurrentPayload | null | undefined;
|
||||||
display: DrawCurrentPayload | null | undefined;
|
display: DrawCurrentPayload | null | undefined;
|
||||||
|
serverNowMs: number;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
reload: () => Promise<void>;
|
reload: () => Promise<void>;
|
||||||
isBettable: boolean;
|
isBettable: boolean;
|
||||||
@@ -48,6 +49,7 @@ function applySnapshotDrift(
|
|||||||
*/
|
*/
|
||||||
export function useHallDrawLive(): HallDrawLiveSnapshot {
|
export function useHallDrawLive(): HallDrawLiveSnapshot {
|
||||||
const [raw, setRaw] = useState<DrawCurrentPayload | null | undefined>(undefined);
|
const [raw, setRaw] = useState<DrawCurrentPayload | null | undefined>(undefined);
|
||||||
|
const [serverNowMs, setServerNowMs] = useState(() => Date.now());
|
||||||
const [emittedAtMs, setEmittedAtMs] = useState(() => Date.now());
|
const [emittedAtMs, setEmittedAtMs] = useState(() => Date.now());
|
||||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -72,17 +74,22 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
|
|||||||
setEmittedAtMs(evt.emitted_at_ms ?? Date.now());
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const d = await getDrawCurrent();
|
const d = await getDrawCurrent();
|
||||||
setRaw(d);
|
updateFromResponse(d);
|
||||||
setEmittedAtMs(Date.now());
|
|
||||||
} catch {
|
} catch {
|
||||||
setError("draw.loadFailedRefresh");
|
setError("draw.loadFailedRefresh");
|
||||||
setRaw(undefined);
|
setRaw(undefined);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [updateFromResponse]);
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -228,5 +235,5 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
|
|||||||
|
|
||||||
const isBettable = display != null && display.status === "open";
|
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_version?: number;
|
||||||
result_source?: string | null;
|
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