From 10bee1b8578aa022e9e9bd9892446bf42a3fdeb3 Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 1 Jun 2026 09:29:02 +0800 Subject: [PATCH] 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. --- ecosystem.config.cjs | 10 +- .../layout/player-notification-bell.tsx | 107 ++++++++++++++++++ src/components/layout/player-panel.tsx | 15 +-- src/features/hall/hall-screen.tsx | 15 +-- .../orders/ticket-order-detail-screen.tsx | 1 + src/features/player/entry-gate.tsx | 33 +++++- .../results/draw-result-detail-screen.tsx | 2 + .../results/draw-results-list-screen.tsx | 2 + src/features/wallet/wallet-logs-block.tsx | 25 +--- src/features/wallet/wallet-logs-screen.tsx | 2 + src/features/wallet/wallet-screen.tsx | 2 + src/hooks/use-pending-wallet-reconcile.ts | 72 ++++++++++++ src/i18n/locales/en/player.json | 4 + src/i18n/locales/ne/player.json | 4 + src/i18n/locales/zh/entry.json | 2 +- src/i18n/locales/zh/player.json | 4 + 16 files changed, 241 insertions(+), 59 deletions(-) create mode 100644 src/components/layout/player-notification-bell.tsx create mode 100644 src/hooks/use-pending-wallet-reconcile.ts diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 62df4a0..3fec4b3 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -38,16 +38,10 @@ module.exports = { env: { NODE_ENV: "production", PORT: "3800", - - // Laravel 根地址(无尾部 /);同机部署填 http://127.0.0.1:8000 - LOTTERY_API_UPSTREAM: "http://127.0.0.1:8000", - - // 构建时需存在;运行时可留空若已在 build 时写入 - // NEXT_PUBLIC_MAIN_SITE_URL: "https://main.yourdomain.com", }, - // PM2 5.2+:可把变量放在 .env,由 PM2 注入(需 npm run build 前也有一份供 Next 编译) - // env_file: path.join(APP_CWD, ".env"), + // LOTTERY_API_UPSTREAM、NEXT_PUBLIC_* 写在 .env;勿在此硬编码 LOTTERY_API_UPSTREAM + env_file: path.join(APP_CWD, ".env"), }, ], }; diff --git a/src/components/layout/player-notification-bell.tsx b/src/components/layout/player-notification-bell.tsx new file mode 100644 index 0000000..16d165a --- /dev/null +++ b/src/components/layout/player-notification-bell.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { Bell } from "lucide-react"; +import Link from "next/link"; +import { useState, type ReactElement } from "react"; +import { useTranslation } from "react-i18next"; + +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { logTypeLabel } from "@/features/wallet/wallet-logs-block"; +import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile"; +import { playerHeaderControl } from "@/lib/player-spacing"; +import { formatMinorAsCurrency } from "@/lib/money"; +import { cn } from "@/lib/utils"; + +type PlayerNotificationBellProps = { + className?: string; +}; + +/** 顶栏铃铛:待对账划转等提醒(Popover 右上角展开) */ +export function PlayerNotificationBell({ className }: PlayerNotificationBellProps): ReactElement { + const { t } = useTranslation("common"); + const { t: tp } = useTranslation("player"); + const [open, setOpen] = useState(false); + const { pending, hasPending, loading, refresh } = usePendingWalletReconcile(); + + return ( + { + setOpen(next); + if (next) void refresh(); + }} + > + + + {hasPending ? ( + + ) : null} + + } + /> + +
+

{t("navigation.notifications")}

+
+ + {loading && pending.length === 0 ? ( +

{tp("actions.loading")}

+ ) : null} + + {!loading && pending.length === 0 ? ( +

+ {tp("notifications.empty")} +

+ ) : null} + + {pending.length > 0 ? ( +
+
+

{tp("wallet.pendingTitle")}

+

+ {tp("wallet.pendingDescription")} +

+
    + {pending.map((p) => ( +
  • + + {logTypeLabel(p.type, tp)}{" "} + {formatMinorAsCurrency(p.amount, p.currency_code)} + + + {tp("wallet.pendingStatus")} + +
  • + ))} +
+
+ setOpen(false)} + > + {tp("wallet.logs", { defaultValue: "查看流水" })} + +
+ ) : null} +
+
+ ); +} diff --git a/src/components/layout/player-panel.tsx b/src/components/layout/player-panel.tsx index 40c0666..950c981 100644 --- a/src/components/layout/player-panel.tsx +++ b/src/components/layout/player-panel.tsx @@ -3,10 +3,11 @@ import Link from "next/link"; import type { ReactNode } from "react"; -import { Bell, ChevronLeft } from "lucide-react"; +import { ChevronLeft } from "lucide-react"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/components/language-switcher"; +import { PlayerNotificationBell } from "@/components/layout/player-notification-bell"; import { playerHeaderControl, playerPageHeader, @@ -79,17 +80,7 @@ export function PlayerPanel({ "rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:py-0 [&_button]:text-xs", )} /> - + {children} diff --git a/src/features/hall/hall-screen.tsx b/src/features/hall/hall-screen.tsx index bcc1840..e63a2d9 100644 --- a/src/features/hall/hall-screen.tsx +++ b/src/features/hall/hall-screen.tsx @@ -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")} - + diff --git a/src/features/orders/ticket-order-detail-screen.tsx b/src/features/orders/ticket-order-detail-screen.tsx index 88d3f1d..b9dfaf5 100644 --- a/src/features/orders/ticket-order-detail-screen.tsx +++ b/src/features/orders/ticket-order-detail-screen.tsx @@ -432,6 +432,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) { {data.draw_no ? ( { if (!effectiveToken) { + // 主站 iframe:token 由 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 (
diff --git a/src/features/results/draw-result-detail-screen.tsx b/src/features/results/draw-result-detail-screen.tsx index a2df6b2..dc5c66a 100644 --- a/src/features/results/draw-result-detail-screen.tsx +++ b/src/features/results/draw-result-detail-screen.tsx @@ -163,6 +163,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) {data.previous_draw_no ? ( {t("results.openDetail", { defaultValue: "查看详情" })} @@ -273,6 +274,7 @@ export function DrawResultsListScreen() {
diff --git a/src/features/wallet/wallet-logs-block.tsx b/src/features/wallet/wallet-logs-block.tsx index 5022c7f..f8d0c3d 100644 --- a/src/features/wallet/wallet-logs-block.tsx +++ b/src/features/wallet/wallet-logs-block.tsx @@ -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 ? ( -
-

{t("wallet.pendingTitle")}

-

- {t("wallet.pendingDescription")} -

-
- {logs.pending_reconcile.map((p) => ( -
- - {logTypeLabel(p.type, t)}{" "} - {formatMinorAsCurrency(p.amount, p.currency_code)} - - {t("wallet.pendingStatus")} -
- ))} -
-
- ) : null} -

{resolvedTitle}

diff --git a/src/features/wallet/wallet-logs-screen.tsx b/src/features/wallet/wallet-logs-screen.tsx index 441954d..4769870 100644 --- a/src/features/wallet/wallet-logs-screen.tsx +++ b/src/features/wallet/wallet-logs-screen.tsx @@ -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) { diff --git a/src/features/wallet/wallet-screen.tsx b/src/features/wallet/wallet-screen.tsx index c4d573a..1ffd16a 100644 --- a/src/features/wallet/wallet-screen.tsx +++ b/src/features/wallet/wallet-screen.tsx @@ -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]); diff --git a/src/hooks/use-pending-wallet-reconcile.ts b/src/hooks/use-pending-wallet-reconcile.ts new file mode 100644 index 0000000..6a2016a --- /dev/null +++ b/src/hooks/use-pending-wallet-reconcile.ts @@ -0,0 +1,72 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { getWalletLogs } from "@/api/wallet"; +import { usePlayerSessionStore } from "@/stores/player-session-store"; +import type { WalletPendingTransfer } from "@/types/api/wallet-logs"; + +export const WALLET_LOGS_REFRESH_EVENT = "lottery:wallet-logs-refreshed"; + +type WalletLogsRefreshDetail = { + pending?: WalletPendingTransfer[]; +}; + +export function usePendingWalletReconcile(): { + pending: WalletPendingTransfer[]; + hasPending: boolean; + loading: boolean; + refresh: () => Promise; +} { + const bearerToken = usePlayerSessionStore((s) => s.bearerToken); + const [pending, setPending] = useState([]); + const [loading, setLoading] = useState(false); + + const refresh = useCallback(async (): Promise => { + if (!bearerToken?.trim()) { + setPending([]); + return; + } + setLoading(true); + try { + const data = await getWalletLogs({ page: 1, size: 1 }); + setPending(data.pending_reconcile ?? []); + } catch { + setPending([]); + } finally { + setLoading(false); + } + }, [bearerToken]); + + useEffect(() => { + void refresh(); + + function onWalletLogsRefreshed(event: Event): void { + const detail = (event as CustomEvent).detail; + if (Array.isArray(detail?.pending)) { + setPending(detail.pending); + return; + } + void refresh(); + } + + window.addEventListener(WALLET_LOGS_REFRESH_EVENT, onWalletLogsRefreshed); + return () => window.removeEventListener(WALLET_LOGS_REFRESH_EVENT, onWalletLogsRefreshed); + }, [refresh]); + + return { + pending, + hasPending: pending.length > 0, + loading, + refresh, + }; +} + +export function dispatchWalletLogsRefresh(pending: WalletPendingTransfer[]): void { + if (typeof window === "undefined") return; + window.dispatchEvent( + new CustomEvent(WALLET_LOGS_REFRESH_EVENT, { + detail: { pending }, + }), + ); +} diff --git a/src/i18n/locales/en/player.json b/src/i18n/locales/en/player.json index a59b81f..a864b64 100644 --- a/src/i18n/locales/en/player.json +++ b/src/i18n/locales/en/player.json @@ -26,6 +26,9 @@ "rules": "Rules", "wallet": "Wallet" }, + "notifications": { + "empty": "No notifications" + }, "panel": { "home": "Home" }, @@ -367,6 +370,7 @@ "flowsTitle": "Wallet logs", "totalRecords": "{{total}} records", "emptyLogs": "No wallet logs", + "noMoreLogs": "No more transactions", "balanceAfter": "Balance after", "wsBalanceUpdated": "Balance {{change}} ({{reason}})", "wsReason": { diff --git a/src/i18n/locales/ne/player.json b/src/i18n/locales/ne/player.json index a5c4ca9..67d1c28 100644 --- a/src/i18n/locales/ne/player.json +++ b/src/i18n/locales/ne/player.json @@ -26,6 +26,9 @@ "rules": "नियम", "wallet": "वालेट" }, + "notifications": { + "empty": "कुनै सूचना छैन" + }, "panel": { "home": "गृह" }, @@ -367,6 +370,7 @@ "flowsTitle": "वालेट लग", "totalRecords": "{{total}} रेकर्ड", "emptyLogs": "वालेट लग छैन", + "noMoreLogs": "थप लेनदेन छैन", "balanceAfter": "पछि बाँकी ब्यालेन्स", "wsBalanceUpdated": "ब्यालेन्स {{change}} ({{reason}})", "wsReason": { diff --git a/src/i18n/locales/zh/entry.json b/src/i18n/locales/zh/entry.json index 5785000..1ff8efd 100644 --- a/src/i18n/locales/zh/entry.json +++ b/src/i18n/locales/zh/entry.json @@ -29,7 +29,7 @@ "retryProgress": "正在重试({{current}}/{{total}})…" }, "failure": { - "title": "授权失败111", + "title": "授权失败", "subtitle": "无法完成授权,请重试。", "detailsTitle": "失败详情", "table": { diff --git a/src/i18n/locales/zh/player.json b/src/i18n/locales/zh/player.json index d44adfb..dba619f 100644 --- a/src/i18n/locales/zh/player.json +++ b/src/i18n/locales/zh/player.json @@ -26,6 +26,9 @@ "rules": "规则", "wallet": "钱包" }, + "notifications": { + "empty": "暂无通知" + }, "panel": { "home": "首页" }, @@ -368,6 +371,7 @@ "flowsTitle": "资金流水", "totalRecords": "共 {{total}} 条记录", "emptyLogs": "暂无流水", + "noMoreLogs": "没有更多流水", "balanceAfter": "变更后余额", "wsBalanceUpdated": "余额 {{change}}({{reason}})", "wsReason": {