This commit is contained in:
@@ -33,7 +33,7 @@ import {
|
||||
PLAY_CATALOG_REFRESH_EVENT,
|
||||
type PlayCatalogRefreshSource,
|
||||
} from "@/lib/play-catalog-events";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
|
||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
@@ -499,20 +499,28 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
const clearPlaceTraceId = useCallback(() => {
|
||||
placeTraceIdRef.current = null;
|
||||
}, []);
|
||||
const catalogSeqRef = useRef(0);
|
||||
const walletSeqRef = useRef(0);
|
||||
|
||||
const loadCatalog = useCallback(async () => {
|
||||
setCatalogState((s) => (s.kind === "ok" ? s : { kind: "loading" }));
|
||||
const seq = ++catalogSeqRef.current;
|
||||
setCatalogState({ kind: "loading" });
|
||||
try {
|
||||
const data = await getPlayEffective({ currency: currencyParam });
|
||||
if (seq !== catalogSeqRef.current) return;
|
||||
setCatalogState({ kind: "ok", data });
|
||||
} catch (e) {
|
||||
if (seq !== catalogSeqRef.current) return;
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.loadingError");
|
||||
setCatalogState({ kind: "error", message: msg });
|
||||
}
|
||||
}, [currencyParam, t]);
|
||||
|
||||
const refreshWallet = useCallback(async () => {
|
||||
const seq = ++walletSeqRef.current;
|
||||
try {
|
||||
const wallet = await getWalletBalance({ currency: currencyParam });
|
||||
if (seq !== walletSeqRef.current) return;
|
||||
setAvailableMinor(Number(wallet.available_balance ?? 0));
|
||||
} catch {
|
||||
// 保留上次可用余额,避免短暂失败导致误报余额不足
|
||||
@@ -526,15 +534,6 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
});
|
||||
}, [loadCatalog, refreshWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => {
|
||||
void loadCatalog();
|
||||
void refreshWallet();
|
||||
};
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
}, [loadCatalog, refreshWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCatalogRefresh = (ev: Event) => {
|
||||
void loadCatalog();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -86,23 +86,28 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
const [siblingItems, setSiblingItems] = useState<TicketItemListRow[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const requestSeqRef = useRef(0);
|
||||
const backHref = "/orders";
|
||||
const backLabel = t("orders.title");
|
||||
|
||||
const load = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const seq = ++requestSeqRef.current;
|
||||
if (!options?.silent) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
const row = await getTicketItemDetail(ticketNo);
|
||||
if (seq !== requestSeqRef.current) return;
|
||||
setData(row);
|
||||
} catch {
|
||||
if (seq !== requestSeqRef.current) return;
|
||||
setData(null);
|
||||
if (!options?.silent) {
|
||||
setError(t("orders.notFound"));
|
||||
}
|
||||
} finally {
|
||||
if (seq !== requestSeqRef.current) return;
|
||||
if (!options?.silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -117,8 +122,14 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
const orderNo = (data?.order_no ?? "").trim();
|
||||
const siblingSeq = requestSeqRef.current;
|
||||
|
||||
if (!orderNo) {
|
||||
setSiblingItems([]);
|
||||
queueMicrotask(() => {
|
||||
if (siblingSeq === requestSeqRef.current) {
|
||||
setSiblingItems([]);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,12 +137,12 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await getTicketItems({ order_no: orderNo, per_page: 50 });
|
||||
if (cancelled) return;
|
||||
if (cancelled || siblingSeq !== requestSeqRef.current) return;
|
||||
setSiblingItems(
|
||||
res.items.filter((row) => row.ticket_no !== ticketNo),
|
||||
);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
if (!cancelled && siblingSeq === requestSeqRef.current) {
|
||||
setSiblingItems([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -79,6 +80,29 @@ function initialSteps(): EntryStep[] {
|
||||
];
|
||||
}
|
||||
|
||||
let pendingUrlToken = "";
|
||||
|
||||
function subscribeUrlTokenCapture(onStoreChange: () => void): () => void {
|
||||
if (typeof window !== "undefined") {
|
||||
const url = new URL(window.location.href);
|
||||
const token = url.searchParams.get("token");
|
||||
if (token) {
|
||||
pendingUrlToken = token;
|
||||
stripSearchParamFromBrowserUrl("token");
|
||||
queueMicrotask(onStoreChange);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
function getUrlTokenSnapshot(): string {
|
||||
return pendingUrlToken;
|
||||
}
|
||||
|
||||
function getServerUrlTokenSnapshot(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
function stripSearchParamFromBrowserUrl(name: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
const url = new URL(window.location.href);
|
||||
@@ -113,6 +137,11 @@ export function EntryGate() {
|
||||
const { t: tc } = useTranslation("common");
|
||||
|
||||
const tokenFromUrl = searchParams.get("token") ?? "";
|
||||
const capturedUrlToken = useSyncExternalStore(
|
||||
subscribeUrlTokenCapture,
|
||||
getUrlTokenSnapshot,
|
||||
getServerUrlTokenSnapshot,
|
||||
);
|
||||
const sessionExpired = searchParams.get("session") === "expired";
|
||||
|
||||
const { bearerToken, setBearerToken, setProfile, setCurrencies, clearBearerToken } =
|
||||
@@ -137,11 +166,11 @@ export function EntryGate() {
|
||||
|
||||
if (!isInIframe()) {
|
||||
if (sessionExpired) return false;
|
||||
return tokenFromUrl !== "";
|
||||
return capturedUrlToken !== "" || tokenFromUrl !== "";
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [sessionExpired, tokenFromUrl]);
|
||||
}, [capturedUrlToken, sessionExpired, tokenFromUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gateReady) return;
|
||||
@@ -169,14 +198,15 @@ export function EntryGate() {
|
||||
const [steps, setSteps] = useState<EntryStep[]>(initialSteps());
|
||||
|
||||
const entryToken = useMemo(() => {
|
||||
const urlToken = capturedUrlToken || tokenFromUrl;
|
||||
if (typeof window === "undefined") {
|
||||
return tokenFromUrl || bearerToken;
|
||||
return urlToken || bearerToken;
|
||||
}
|
||||
if (!isInIframe()) {
|
||||
return tokenFromUrl;
|
||||
return urlToken;
|
||||
}
|
||||
return tokenFromUrl || bearerToken;
|
||||
}, [bearerToken, tokenFromUrl]);
|
||||
return urlToken || bearerToken;
|
||||
}, [bearerToken, capturedUrlToken, tokenFromUrl]);
|
||||
/** 防止 token 写入 store / URL 剥离后重复触发进场,避免成功/失败页闪一下 */
|
||||
const entryLifecycleRef = useRef<"idle" | "running" | "done">("idle");
|
||||
|
||||
@@ -215,8 +245,10 @@ export function EntryGate() {
|
||||
setPhase("loading");
|
||||
setFailureDetails([]);
|
||||
|
||||
if (tokenFromUrl) {
|
||||
setBearerToken(tokenFromUrl);
|
||||
const urlToken = capturedUrlToken || tokenFromUrl;
|
||||
if (urlToken) {
|
||||
setBearerToken(urlToken);
|
||||
pendingUrlToken = "";
|
||||
stripSearchParamFromBrowserUrl("token");
|
||||
}
|
||||
|
||||
@@ -311,6 +343,7 @@ export function EntryGate() {
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
capturedUrlToken,
|
||||
entryToken,
|
||||
tokenFromUrl,
|
||||
setBearerToken,
|
||||
|
||||
@@ -63,11 +63,13 @@ export function WalletScreen() {
|
||||
|
||||
actionDeepLinkHandledRef.current = true;
|
||||
if (!isCreditPlayer) {
|
||||
if (action === "transfer-in") {
|
||||
setTransferInOpen(true);
|
||||
} else {
|
||||
setTransferOutOpen(true);
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
if (action === "transfer-in") {
|
||||
setTransferInOpen(true);
|
||||
} else {
|
||||
setTransferOutOpen(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
router.replace("/wallet", { scroll: false });
|
||||
}, [isCreditPlayer, loading, router, searchParams]);
|
||||
@@ -78,7 +80,7 @@ export function WalletScreen() {
|
||||
if (section !== "logs" && section !== "pending") return;
|
||||
|
||||
sectionDeepLinkHandledRef.current = true;
|
||||
const targetId = "wallet-logs";
|
||||
const targetId = section === "pending" ? "wallet-pending" : "wallet-logs";
|
||||
const timer = window.setTimeout(() => {
|
||||
scrollToWalletSection(targetId);
|
||||
router.replace("/wallet", { scroll: false });
|
||||
@@ -332,6 +334,42 @@ export function WalletScreen() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isCreditPlayer && (logs?.pending_reconcile?.length ?? 0) > 0 ? (
|
||||
<section
|
||||
id="wallet-pending"
|
||||
className="scroll-mt-3 rounded-2xl border border-amber-200 bg-amber-50 px-3 py-3 text-sm text-amber-900"
|
||||
>
|
||||
<h2 className="text-sm font-black text-amber-800">
|
||||
{t("wallet.pendingSectionTitle", { defaultValue: "待对账划转" })}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-amber-700">
|
||||
{t("wallet.pendingSectionHint", {
|
||||
defaultValue: "以下划转仍在与主站对账,请稍后刷新查看结果。",
|
||||
})}
|
||||
</p>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{logs?.pending_reconcile.map((item) => (
|
||||
<li
|
||||
key={item.transfer_no}
|
||||
className="rounded-xl border border-amber-200 bg-white px-3 py-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="font-semibold">
|
||||
{item.direction === "in"
|
||||
? t("wallet.flow.transfer_in", { defaultValue: "转入" })
|
||||
: t("wallet.flow.transfer_out", { defaultValue: "转出" })}
|
||||
</span>
|
||||
<span className="font-black tabular-nums text-amber-800">
|
||||
{formatMinorAsCurrency(item.amount, item.currency_code || currency)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 font-mono text-[11px] text-amber-600">{item.transfer_no}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div id="wallet-logs" className="scroll-mt-3">
|
||||
<WalletLogsBlock
|
||||
creditMode={isCreditPlayer}
|
||||
|
||||
Reference in New Issue
Block a user