fix: 优化嵌入登录等待态并修复大厅与订单筛选交互

This commit is contained in:
2026-06-08 17:42:04 +08:00
parent 36adf8699d
commit 2ddba8462e
13 changed files with 71 additions and 75 deletions

View File

@@ -26,7 +26,6 @@ import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { triggerWalletPollingAfterBet } from "@/hooks/use-wallet-polling";
import { getLotteryEcho } from "@/lib/lottery-echo";
import { getLotteryRequestLocale } from "@/lib/lottery-locale";
import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money";
import { playLabel } from "@/lib/play-labels";
import {
@@ -459,6 +458,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const [resultOpen, setResultOpen] = useState(false);
const [resultData, setResultData] = useState<TicketPlaceData | null>(null);
const [quickFillState, setQuickFillState] = useState<QuickFillState>(() => loadQuickFillState());
const [riskStateDrawNo, setRiskStateDrawNo] = useState<string | null>(display?.draw_no ?? null);
const [liveSoldOutNumbers, setLiveSoldOutNumbers] = useState<Set<string>>(() => new Set());
const [liveWarningNumbers, setLiveWarningNumbers] = useState<Set<string>>(() => new Set());
const [debouncedSummary, setDebouncedSummary] = useState({ bet: 0, rebate: 0, actual: 0 });
@@ -475,9 +475,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
? crypto.randomUUID()
: `pl-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const clearPlaceTraceId = () => {
const clearPlaceTraceId = useCallback(() => {
placeTraceIdRef.current = null;
};
}, []);
const loadCatalog = useCallback(async () => {
setCatalogState((s) => (s.kind === "ok" ? s : { kind: "loading" }));
try {
@@ -576,34 +576,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const drawNo = display?.draw_no ?? null;
useEffect(() => {
if (riskStateDrawNo !== drawNo) {
setRiskStateDrawNo(drawNo);
setLiveSoldOutNumbers(new Set());
setLiveWarningNumbers(new Set());
}, [drawNo]);
}
const alertRows = display?.risk_pool_alerts ?? [];
const jackpot = display?.jackpot;
const currentQuickFill = quickFillState[activeCategory] ?? { favorites: [], history: [] };
const favorites = currentQuickFill.favorites;
const historyNumbers = currentQuickFill.history;
const jackpotPanelCopy = !jackpot?.enabled
? {
title: t("hall.jackpotPanel.disabledTitle"),
subtitle: t("hall.jackpotPanel.disabledSubtitle"),
description: t("hall.jackpotPanel.disabledDescription"),
}
: isBettable
? {
title: t("hall.jackpotPanel.infoTitle"),
subtitle: t("hall.jackpotPanel.infoSubtitle"),
description: t("hall.jackpotPanel.infoDescription"),
}
: {
title: t("hall.jackpotPanel.closedInfoTitle"),
subtitle: t("hall.jackpotPanel.closedInfoSubtitle"),
description: t("hall.jackpotPanel.closedInfoDescription"),
};
const tableDisabled = !isBettable || catalogState.kind !== "ok";
const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status));
const defaultNumberPlaceholder =
@@ -715,8 +698,18 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
};
const clearAmountsForPlay = useCallback((playCode: string): boolean => {
let removed = false;
const hasAmountsForPlay = useCallback(
(playCode: string): boolean =>
rows.some((row) =>
Object.entries(row.amounts).some(
([amountKey, amountValue]) =>
amountKey.split("@")[0] === playCode && Boolean(amountValue?.trim()),
),
),
[rows],
);
const clearAmountsForPlay = useCallback((playCode: string) => {
setRows((current) =>
current.map((row) => {
const nextAmounts = { ...row.amounts };
@@ -726,13 +719,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (keyPlayCode !== playCode || !nextAmounts[amountKey]) return;
nextAmounts[amountKey] = "";
changed = true;
removed = true;
});
return changed ? { ...row, amounts: nextAmounts } : row;
}),
);
return removed;
}, []);
useEffect(() => {
@@ -743,7 +733,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const onPlayToggle = (evt: PlayToggleWsEvent) => {
if (evt.enabled === false && typeof evt.play_code === "string") {
const removed = clearAmountsForPlay(evt.play_code);
const removed = hasAmountsForPlay(evt.play_code);
clearAmountsForPlay(evt.play_code);
setPreviewOpen(false);
setPreviewData(null);
clearPlaceTraceId();
@@ -809,7 +800,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
channel.stopListening(".risk.sold_out");
channel.stopListening(".risk.warning");
};
}, [clearAmountsForPlay, drawNo, loadCatalog, reloadDraw, t]);
}, [clearAmountsForPlay, clearPlaceTraceId, drawNo, hasAmountsForPlay, reloadDraw, t]);
const collectDraftLineIssues = useCallback((): DraftLineIssue[] => {
if (activeCategory === "JACKPOT") return [];

View File

@@ -144,7 +144,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
window.removeEventListener("lottery-hall-refresh", refresh);
window.removeEventListener("lottery-wallet-refresh", refresh);
};
}, [data?.status, load]);
}, [data, load]);
if (loading) {
return (

View File

@@ -69,7 +69,10 @@ export function TicketOrdersListScreen() {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const [queryDrawNo, setQueryDrawNo] = useState(drawNoFilter);
const [queryDrawNoState, setQueryDrawNoState] = useState(() => ({
base: drawNoFilter,
draft: drawNoFilter,
}));
const [queryNumber, setQueryNumber] = useState("");
const [queryStatuses, setQueryStatuses] = useState<string[]>(statusFilter);
const [fromDate, setFromDate] = useState("");
@@ -82,9 +85,9 @@ export function TicketOrdersListScreen() {
const isMobile = useIsMobile();
const initialLoadDone = useRef(false);
useEffect(() => {
setQueryDrawNo(drawNoFilter);
}, [drawNoFilter]);
const queryDrawNoInput =
queryDrawNoState.base === drawNoFilter ? queryDrawNoState.draft : drawNoFilter;
const queryDrawNo = queryDrawNoInput || drawNoFilter;
const selectedRange = useMemo(() => {
const from = parseSchedulePickerYmd(fromDate);
@@ -118,7 +121,7 @@ export function TicketOrdersListScreen() {
const res = await getTicketItems({
page: nextPage,
per_page: ORDERS_PAGE_SIZE,
draw_no: queryDrawNo || drawNoFilter || undefined,
draw_no: queryDrawNo || undefined,
number: queryNumber || undefined,
status: queryStatuses.length ? queryStatuses : undefined,
start_date: fromDate || undefined,
@@ -136,7 +139,7 @@ export function TicketOrdersListScreen() {
setLoadingMore(false);
}
},
[drawNoFilter, fromDate, queryDrawNo, queryNumber, queryStatuses, t, toDate],
[fromDate, queryDrawNo, queryNumber, queryStatuses, t, toDate],
);
useEffect(() => {
@@ -219,13 +222,13 @@ export function TicketOrdersListScreen() {
>
{t("orders.betNow")}
</Link>
{(queryDrawNo || queryNumber || fromDate || toDate || queryStatuses.length > 0) ? (
{(queryDrawNoInput || queryNumber || fromDate || toDate || queryStatuses.length > 0) ? (
<Button
type="button"
variant="outline"
className="h-9 rounded-full border-[#dce7f7] bg-white px-3 text-xs font-bold text-[#32518d] hover:bg-[#f8fbff]"
onClick={() => {
setQueryDrawNo(drawNoFilter);
setQueryDrawNoState({ base: drawNoFilter, draft: drawNoFilter });
setQueryNumber("");
setFromDate("");
setToDate("");
@@ -243,8 +246,10 @@ export function TicketOrdersListScreen() {
<div className="mt-3 grid grid-cols-2 gap-2 lg:grid-cols-4">
<div className="flex h-9 min-w-0 items-center rounded-full border border-[#dce7f7] bg-[#fbfdff] px-3">
<Input
value={queryDrawNo}
onChange={(e) => setQueryDrawNo(e.target.value)}
value={queryDrawNoInput}
onChange={(e) =>
setQueryDrawNoState({ base: drawNoFilter, draft: e.target.value })
}
placeholder={t("orders.drawNo")}
aria-label={t("orders.drawNo")}
className="h-7 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0"

View File

@@ -109,6 +109,12 @@ export function EntryGate() {
const { bearerToken, setBearerToken, setProfile, setCurrencies, clearBearerToken } =
usePlayerSessionStore();
const waitingForEmbeddedToken =
!sessionExpired &&
typeof window !== "undefined" &&
isInIframe() &&
!tokenFromUrl &&
!(bearerToken ?? "").trim();
const [phase, setPhase] = useState<Phase>(sessionExpired ? "failed" : "loading");
const [failureDetails, setFailureDetails] = useState<FailureRow[]>(() =>
@@ -273,19 +279,12 @@ export function EntryGate() {
}, [sessionExpired, clearBearerToken]);
useEffect(() => {
if (sessionExpired) return;
const embedded = typeof window !== "undefined" && isInIframe() && !tokenFromUrl;
if (embedded && !effectiveToken) {
setPhase("loading");
return;
}
if (sessionExpired || waitingForEmbeddedToken) return;
const tmr = window.setTimeout(() => {
void doEntry();
}, 300);
return () => window.clearTimeout(tmr);
}, [doEntry, sessionExpired, effectiveToken, tokenFromUrl]);
}, [doEntry, sessionExpired, waitingForEmbeddedToken]);
useEffect(() => {
if (sessionExpired) return;
@@ -320,7 +319,7 @@ export function EntryGate() {
</div>
<div className="flex flex-1 flex-col px-3 py-5">
{phase === "loading" ? (
{phase === "loading" || waitingForEmbeddedToken ? (
<div className="mx-auto w-full max-w-md">
<div className="mb-6 flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded bg-red-600 text-white">

View File

@@ -24,10 +24,6 @@ export function NotificationsScreen() {
return (
<PlayerPanel title={t("notifications.title")} backHref="/hall">
<div className="space-y-3">
<p className="rounded-xl border border-amber-200 bg-amber-50/90 px-3 py-2.5 text-xs leading-relaxed text-amber-900">
{t("notifications.subtitle")}
</p>
<div className="flex items-center justify-between rounded-xl border border-[#dce7f7] bg-[#f8fbff] px-3 py-2.5">
<p className="text-sm font-semibold text-[#0b3f96]">
{t("notifications.unreadCount", { count: unreadCount })}

View File

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { getWalletBalance, getWalletLogs } from "@/api/wallet";