refactor: update environment configuration and enhance notification handling

- Refactored ecosystem configuration to utilize .env for sensitive variables, improving security and flexibility.
- Replaced direct notification button implementation with a dedicated PlayerNotificationBell component for better code organization.
- Updated various screens to integrate the new notification component and adjusted prefetch settings for links to optimize performance.
- Added new translations for notifications and wallet logs to enhance user experience across multiple languages.
This commit is contained in:
2026-06-01 09:29:02 +08:00
parent 9f43d07778
commit 10bee1b857
16 changed files with 241 additions and 59 deletions

View File

@@ -1,12 +1,12 @@
"use client";
import { Bell } 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 { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { HallDrawPanel } from "@/features/hall/hall-draw-panel";
@@ -21,7 +21,6 @@ import { cn } from "@/lib/utils";
* 下注大厅:钱包条 §4 + 当期期号 §4.2(封盘置灰 / 倒计时错误色 / WS+轮询);玩法目录 §12.3;下注表格 §13.3。
*/
export function HallScreen() {
const { t } = useTranslation("common");
const { t: tp } = useTranslation("player");
const drawLive = useHallDrawLive();
const { activeCurrency } = useActivePlayerCurrency();
@@ -69,17 +68,7 @@ export function HallScreen() {
>
{tp("nav.rules")}
</Link>
<button
type="button"
className={cn(
playerHeaderControl,
"relative size-8 rounded-full text-[#1d57b7] hover:bg-[#f4f7fb]",
)}
aria-label={t("navigation.notifications")}
>
<Bell className="size-4" aria-hidden />
<span className="absolute right-1.5 top-1.5 size-1.5 rounded-full bg-[#ff143d]" />
</button>
<PlayerNotificationBell />
</div>
</header>

View File

@@ -432,6 +432,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
{data.draw_no ? (
<Link
href={`/results/${encodeURIComponent(data.draw_no)}`}
prefetch={false}
className={cn(
"inline-flex h-11 min-w-[140px] flex-1 items-center justify-center rounded-xl bg-[#07459f] px-4 text-sm font-bold text-white shadow-sm transition-colors hover:bg-[#063b88]",
)}

View File

@@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
import { getPublicCurrencies } from "@/api/currency";
import { getPlayerMe, getPlayerPing } from "@/api/player";
import { isInIframe } from "@/components/iframe-bridge";
import { LanguageSwitcher } from "@/components/language-switcher";
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -25,6 +26,8 @@ import { LotteryApiBizError } from "@/types/api/errors";
const RETRY_ATTEMPTS = 3;
const RETRY_DELAY_MS = 2000;
/** 嵌入主站 iframe 时,等待父页 postMessage 下发 token 的最长时间 */
const IFRAME_TOKEN_WAIT_MS = 15_000;
const MAIN_SITE_URL = process.env.NEXT_PUBLIC_MAIN_SITE_URL?.trim() ?? "";
@@ -134,11 +137,18 @@ export function EntryGate() {
const doEntry = useCallback(async () => {
if (!effectiveToken) {
// 主站 iframetoken 由 MAIN_INIT_TOKEN 稍后到达,勿先闪「授权失败」
if (typeof window !== "undefined" && isInIframe() && !tokenFromUrl) {
return;
}
setPhase("failed");
setFailureDetails([{ code: "NO_TOKEN", detailKey: "errors.noTokenDetail" }]);
return;
}
setPhase("loading");
setFailureDetails([]);
if (tokenFromUrl) {
setBearerToken(tokenFromUrl);
stripSearchParamFromBrowserUrl("token");
@@ -260,11 +270,32 @@ export function EntryGate() {
useEffect(() => {
if (sessionExpired) return;
const embedded = typeof window !== "undefined" && isInIframe() && !tokenFromUrl;
if (embedded && !effectiveToken) {
setPhase("loading");
return;
}
const tmr = window.setTimeout(() => {
void doEntry();
}, 300);
return () => window.clearTimeout(tmr);
}, [doEntry, sessionExpired]);
}, [doEntry, sessionExpired, effectiveToken, tokenFromUrl]);
useEffect(() => {
if (sessionExpired) return;
if (tokenFromUrl || effectiveToken) return;
if (typeof window === "undefined" || !isInIframe()) return;
const tmr = window.setTimeout(() => {
if (usePlayerSessionStore.getState().bearerToken?.trim()) return;
setFailureDetails([{ code: "NO_TOKEN", detailKey: "errors.noTokenDetail" }]);
setPhase("failed");
}, IFRAME_TOKEN_WAIT_MS);
return () => window.clearTimeout(tmr);
}, [sessionExpired, effectiveToken, tokenFromUrl]);
return (
<div className="relative flex min-h-dvh flex-col bg-white">

View File

@@ -163,6 +163,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
{data.previous_draw_no ? (
<Link
href={`/results/${encodeURIComponent(data.previous_draw_no)}`}
prefetch={false}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",
@@ -188,6 +189,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
{data.next_draw_no ? (
<Link
href={`/results/${encodeURIComponent(data.next_draw_no)}`}
prefetch={false}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",

View File

@@ -252,6 +252,7 @@ export function DrawResultsListScreen() {
</div>
<Link
href={`/results/${encodeURIComponent(featured.draw_no)}`}
prefetch={false}
className="inline-flex h-8 shrink-0 items-center justify-center rounded-full border border-[#dce7f7] bg-white px-3 text-sm font-semibold text-[#0b56b7] transition-colors hover:bg-[#f1f6ff]"
>
{t("results.openDetail", { defaultValue: "查看详情" })}
@@ -273,6 +274,7 @@ export function DrawResultsListScreen() {
<Link
key={row.draw_no}
href={`/results/${encodeURIComponent(row.draw_no)}`}
prefetch={false}
className="block rounded-xl border border-[#e5edf8] bg-white p-3 shadow-[0_8px_24px_rgba(15,23,42,0.05)] transition-colors hover:border-[#b9ccf6]"
>
<div className="flex items-start justify-between gap-3">

View File

@@ -48,7 +48,7 @@ type WalletLogsBlockProps = {
title?: string;
};
/** 待对账 + 类型筛选 + 列表(供钱包主页与 `/wallet/logs` 共用 */
/** 类型筛选 + 列表(待对账见顶栏通知铃铛 */
export function WalletLogsBlock({
logs,
logsLoading,
@@ -74,29 +74,6 @@ export function WalletLogsBlock({
return (
<>
{logs && logs.pending_reconcile.length > 0 ? (
<section className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-3">
<p className="text-sm font-black text-amber-700">{t("wallet.pendingTitle")}</p>
<p className="mt-1 text-xs text-amber-700/80">
{t("wallet.pendingDescription")}
</p>
<div className="mt-2 space-y-2 text-sm">
{logs.pending_reconcile.map((p) => (
<div
key={p.transfer_no}
className="flex flex-wrap items-baseline justify-between gap-1 border-b border-dashed border-border py-2 last:border-0"
>
<span className="text-muted-foreground">
{logTypeLabel(p.type, t)}{" "}
{formatMinorAsCurrency(p.amount, p.currency_code)}
</span>
<span className="text-xs text-amber-700">{t("wallet.pendingStatus")}</span>
</div>
))}
</div>
</section>
) : null}
<section className="space-y-3">
<div className="flex flex-col gap-2">
<h2 className="text-sm font-black text-[#0b3f96]">{resolvedTitle}</h2>

View File

@@ -7,6 +7,7 @@ import { getWalletLogs } from "@/api/wallet";
import { Button } from "@/components/ui/button";
import { PlayerPanel } from "@/components/layout/player-panel";
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
import { dispatchWalletLogsRefresh } from "@/hooks/use-pending-wallet-reconcile";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { formatWalletClientError } from "@/lib/wallet-api-error";
@@ -49,6 +50,7 @@ export function WalletLogsScreen() {
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
: nextLogs,
);
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
} catch (e) {
setError(formatWalletClientError(e, t));
if (!append) {

View File

@@ -14,6 +14,7 @@ import {
TransferOutDialog,
} from "@/features/wallet/wallet-transfer-dialogs";
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
import { dispatchWalletLogsRefresh } from "@/hooks/use-pending-wallet-reconcile";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { formatMinorAsCurrency } from "@/lib/money";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
@@ -49,6 +50,7 @@ export function WalletScreen() {
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
: nextLogs,
);
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
return nextLogs;
}, [currency, filter]);