feat: enhance API error handling and UI improvements
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Updated `getPublicCurrencies` and `getPlayerAuthCaptcha` functions to include `skipGlobalServerError` option, improving error handling for API requests.
- Refactored the NotFoundPage component to enhance mobile responsiveness and UI consistency, integrating `PlayerMobileViewport`.
- Improved the NetworkStatusBanner and OfflineBanner components by adding player banner height reference for better layout management.
- Updated Wallet components to utilize `PlayerMoneyDisplay` for consistent currency representation and added credit mode handling in various screens.
- Enhanced the WalletScreen and Transfer screens with loading panels and credit guard logic for better user experience during wallet operations.
This commit is contained in:
2026-06-22 10:50:52 +08:00
parent 34b851d7e1
commit ecb032f8cc
43 changed files with 756 additions and 297 deletions

View File

@@ -23,6 +23,7 @@ type HallBetResultDialogProps = {
currencyCode: string;
data: TicketPlaceData | null;
jackpotEnabled?: boolean;
creditMode?: boolean;
};
const SUCCESS_ITEM_STATUSES = new Set(["pending_draw", "placed"]);
@@ -34,6 +35,7 @@ export function HallBetResultDialog({
currencyCode,
data,
jackpotEnabled = false,
creditMode = false,
}: HallBetResultDialogProps) {
const { t } = useTranslation("player");
@@ -178,7 +180,10 @@ export function HallBetResultDialog({
<span className="font-mono font-black text-[#0b3f96]">{data.order_no}</span>
</p>
<p>
{t("hall.result.balanceAfter")}:{" "}
{creditMode
? t("hall.result.creditBalanceAfter", { defaultValue: "可用信用" })
: t("hall.result.balanceAfter")}
:{" "}
<span className="font-semibold text-slate-950">
{formatMinorAsCurrency(data.balance_after, currencyCode)}
</span>
@@ -200,7 +205,9 @@ export function HallBetResultDialog({
<table className="min-w-[430px] w-full border-collapse text-xs">
<thead className="bg-[#f4f7fd] text-[#304f86]">
<tr>
<th className="w-10 border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">No.</th>
<th className="w-10 border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.table.no")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.result.number")}
</th>

View File

@@ -25,6 +25,7 @@ import {
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 { usePlayerSessionStore } from "@/stores/player-session-store";
import { getLotteryEcho } from "@/lib/lottery-echo";
import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money";
import { playLabel } from "@/lib/play-labels";
@@ -33,6 +34,7 @@ import {
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";
import type { PlayEffectivePayload, PlayEffectivePlayRow } from "@/types/api/play-effective";
@@ -441,6 +443,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const { display, isBettable, reload: reloadDraw } = drawLive;
const { t } = useTranslation("player");
const { activeCurrency: currencyParam } = useActivePlayerCurrency();
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]);
@@ -1241,7 +1244,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
<button
key={`fav-${number}`}
type="button"
className="inline-flex h-7 items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
className="inline-flex h-7 touch-manipulation items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
onPointerDown={() => {
const current = holdFavoriteRef.current;
current.number = number;
@@ -1568,6 +1571,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
currencyCode={currencyCode}
data={resultData}
jackpotEnabled={Boolean(jackpot?.enabled)}
creditMode={creditMode}
/>
</>
);

View File

@@ -207,7 +207,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
/>
<Hourglass
className={cn(
"absolute right-2 top-1/2 size-5 -translate-y-1/2",
"absolute right-0.5 top-1/2 size-4 -translate-y-1/2 opacity-80",
sealedUi ? "text-[#ff143d]" : "text-red-300",
)}
aria-hidden

View File

@@ -1,20 +1,22 @@
"use client";
import { BookOpen } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { CurrencySwitcher } from "@/components/currency-switcher";
import { LanguageSwitcher } from "@/components/language-switcher";
import { PlayerNotificationBell } from "@/components/layout/player-notification-bell";
import { HallBettingGrid } from "@/features/hall/hall-betting-grid";
import { usePlayerStickyTopStyle } from "@/hooks/use-player-sticky-top-style";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { HallDrawPanel } from "@/features/hall/hall-draw-panel";
import { HallWalletStrip } from "@/features/hall/hall-wallet-strip";
import { JackpotBurstOverlay } from "@/features/hall/jackpot-burst-overlay";
import { useHallDrawLive } from "@/features/hall/use-hall-draw-live";
import { useJackpotBurstLive } from "@/features/hall/use-jackpot-burst-live";
import { playerHeaderControl, playerPageInset } from "@/lib/player-spacing";
import { playerHeaderControl, playerPageHeader, playerPageInset } from "@/lib/player-spacing";
import { playerViewportColumnClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
/**
@@ -25,22 +27,29 @@ export function HallScreen() {
const drawLive = useHallDrawLive();
const { activeCurrency } = useActivePlayerCurrency();
const { burstEvent, clearBurstEvent } = useJackpotBurstLive(tp);
const stickyTopStyle = usePlayerStickyTopStyle();
return (
<div className="mx-auto w-full max-w-[480px]">
<div className={playerViewportColumnClass}>
<section className={cn("bg-white text-slate-900", playerPageInset)}>
<header className="relative z-20 mb-2 flex min-h-9 items-center gap-2 overflow-visible">
<header
className={cn(
playerPageHeader,
"sticky z-20 mb-2 flex min-h-9 items-center gap-2 overflow-visible bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur",
)}
style={stickyTopStyle}
>
<div className="flex min-w-0 flex-1 items-center">
<Image
src="/logo.png"
alt="Nlotto"
width={243}
height={84}
className="h-8 w-auto max-w-[min(100%,200px)] object-contain object-left"
className="h-7 w-auto max-w-[min(100%,160px)] object-contain object-left sm:h-8 sm:max-w-[min(100%,200px)]"
priority
/>
</div>
<div className="flex shrink-0 items-center gap-1">
<div className="flex shrink-0 items-center gap-0.5 sm:gap-1">
<CurrencySwitcher
variant="minimal"
menuAlign="end"
@@ -65,10 +74,11 @@ export function HallScreen() {
playerHeaderControl,
"rounded-full border border-[#e4eaf4] bg-[#f8fafc] px-2.5 text-xs font-bold text-[#0b3f96] hover:bg-[#f1f6ff]",
)}
aria-label={tp("nav.rules")}
>
{tp("nav.rules")}
<BookOpen className="size-3.5 shrink-0 sm:hidden" aria-hidden />
<span className="hidden sm:inline">{tp("nav.rules")}</span>
</Link>
<PlayerNotificationBell />
</div>
</header>

View File

@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { useSWRConfig } from "swr";
import { getWalletBalance } from "@/api/wallet";
import { PlayerMoneyDisplay } from "@/components/player-money-display";
import { Skeleton } from "@/components/ui/skeleton";
import {
TransferInDialog,
@@ -65,29 +66,6 @@ export function HallWalletStrip() {
const balanceMinor = Number(balance?.balance ?? 0);
const headlineMinor = isCreditPlayer ? availableMinor : balanceMinor;
const transferInLotteryMinor = isCreditPlayer ? availableMinor : balanceMinor;
// #region agent log
if (typeof window !== "undefined" && balance && !loading) {
fetch("http://127.0.0.1:7696/ingest/e56128e6-898b-4d61-b06d-2aefe0923744", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "7fd0fc" },
body: JSON.stringify({
sessionId: "7fd0fc",
runId: "hall-wallet-display-post-fix",
hypothesisId: "H3",
location: "hall-wallet-strip.tsx:render",
message: "hall wallet headline amounts",
data: {
isCreditPlayer,
availableMinor,
balanceMinor,
headlineMinor,
mismatchWithWalletPage: !isCreditPlayer && headlineMinor !== balanceMinor,
},
timestamp: Date.now(),
}),
}).catch(() => {});
}
// #endregion
const mainMinor =
balance?.main_balance === null || balance?.main_balance === undefined
? null
@@ -115,7 +93,7 @@ export function HallWalletStrip() {
aria-hidden
/>
<div className="relative flex items-center gap-3">
<div className="flex size-13 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
<div className="flex size-14 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
<Wallet className="size-7" aria-hidden />
</div>
<div className="min-w-0 flex-1">
@@ -127,10 +105,21 @@ export function HallWalletStrip() {
{loading ? (
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
) : (
<p className="mt-1 text-2xl font-black leading-none tabular-nums tracking-normal">
{formatMinorAsCurrency(headlineMinor, currency)}
</p>
<PlayerMoneyDisplay
amountMinor={headlineMinor}
currency={currency}
className="mt-1 text-white"
/>
)}
{isCreditPlayer && !loading && balance ? (
<p className="mt-2 text-xs text-white/75">
{t("wallet.creditSummary", {
defaultValue: "授信 {{limit}} · 已用 {{used}}",
limit: formatMinorAsCurrency(balance.credit_limit ?? 0, currency),
used: formatMinorAsCurrency(balance.used_credit ?? 0, currency),
})}
</p>
) : null}
</div>
</div>
</div>