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:
@@ -38,16 +38,10 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
PORT: "3800",
|
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 编译)
|
// LOTTERY_API_UPSTREAM、NEXT_PUBLIC_* 写在 .env;勿在此硬编码 LOTTERY_API_UPSTREAM
|
||||||
// env_file: path.join(APP_CWD, ".env"),
|
env_file: path.join(APP_CWD, ".env"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
107
src/components/layout/player-notification-bell.tsx
Normal file
107
src/components/layout/player-notification-bell.tsx
Normal file
@@ -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 (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(next) => {
|
||||||
|
setOpen(next);
|
||||||
|
if (next) void refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger
|
||||||
|
render={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
playerHeaderControl,
|
||||||
|
"relative size-8 rounded-full text-[#1d57b7] hover:bg-[#f4f7fb]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
aria-label={t("navigation.notifications")}
|
||||||
|
>
|
||||||
|
<Bell className="size-4" aria-hidden />
|
||||||
|
{hasPending ? (
|
||||||
|
<span className="absolute right-1.5 top-1.5 size-1.5 rounded-full bg-[#ff143d]" />
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PopoverContent
|
||||||
|
align="end"
|
||||||
|
sideOffset={8}
|
||||||
|
className="w-[min(20rem,calc(100vw-1.5rem))] border-[#dce7f7] p-0 shadow-[0_16px_40px_rgba(15,23,42,0.14)]"
|
||||||
|
>
|
||||||
|
<div className="border-b border-[#edf2f9] px-3 py-2.5">
|
||||||
|
<p className="text-sm font-black text-[#0b3f96]">{t("navigation.notifications")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && pending.length === 0 ? (
|
||||||
|
<p className="px-3 py-6 text-center text-xs text-slate-500">{tp("actions.loading")}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!loading && pending.length === 0 ? (
|
||||||
|
<p className="px-3 py-6 text-center text-xs text-slate-500">
|
||||||
|
{tp("notifications.empty")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{pending.length > 0 ? (
|
||||||
|
<div className="max-h-[min(50vh,320px)] overflow-y-auto p-2">
|
||||||
|
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-3">
|
||||||
|
<p className="text-sm font-black text-amber-700">{tp("wallet.pendingTitle")}</p>
|
||||||
|
<p className="mt-1 text-xs leading-relaxed text-amber-800/90">
|
||||||
|
{tp("wallet.pendingDescription")}
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 space-y-2">
|
||||||
|
{pending.map((p) => (
|
||||||
|
<li
|
||||||
|
key={p.transfer_no}
|
||||||
|
className="flex flex-wrap items-baseline justify-between gap-1 border-b border-dashed border-amber-200/80 py-2 last:border-0"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-amber-950">
|
||||||
|
{logTypeLabel(p.type, tp)}{" "}
|
||||||
|
{formatMinorAsCurrency(p.amount, p.currency_code)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-semibold text-amber-700">
|
||||||
|
{tp("wallet.pendingStatus")}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/wallet/logs"
|
||||||
|
className="mt-2 block rounded-lg px-2 py-2 text-center text-xs font-bold text-[#0b56b7] hover:bg-[#f8fbff]"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
{tp("wallet.logs", { defaultValue: "查看流水" })}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Bell, ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
|
import { PlayerNotificationBell } from "@/components/layout/player-notification-bell";
|
||||||
import {
|
import {
|
||||||
playerHeaderControl,
|
playerHeaderControl,
|
||||||
playerPageHeader,
|
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",
|
"rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:py-0 [&_button]:text-xs",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<button
|
<PlayerNotificationBell />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Bell } from "lucide-react";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { CurrencySwitcher } from "@/components/currency-switcher";
|
import { CurrencySwitcher } from "@/components/currency-switcher";
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
|
import { PlayerNotificationBell } from "@/components/layout/player-notification-bell";
|
||||||
import { HallBettingGrid } from "@/features/hall/hall-betting-grid";
|
import { HallBettingGrid } from "@/features/hall/hall-betting-grid";
|
||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import { HallDrawPanel } from "@/features/hall/hall-draw-panel";
|
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。
|
* 下注大厅:钱包条 §4 + 当期期号 §4.2(封盘置灰 / 倒计时错误色 / WS+轮询);玩法目录 §12.3;下注表格 §13.3。
|
||||||
*/
|
*/
|
||||||
export function HallScreen() {
|
export function HallScreen() {
|
||||||
const { t } = useTranslation("common");
|
|
||||||
const { t: tp } = useTranslation("player");
|
const { t: tp } = useTranslation("player");
|
||||||
const drawLive = useHallDrawLive();
|
const drawLive = useHallDrawLive();
|
||||||
const { activeCurrency } = useActivePlayerCurrency();
|
const { activeCurrency } = useActivePlayerCurrency();
|
||||||
@@ -69,17 +68,7 @@ export function HallScreen() {
|
|||||||
>
|
>
|
||||||
{tp("nav.rules")}
|
{tp("nav.rules")}
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<PlayerNotificationBell />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
|||||||
{data.draw_no ? (
|
{data.draw_no ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/results/${encodeURIComponent(data.draw_no)}`}
|
href={`/results/${encodeURIComponent(data.draw_no)}`}
|
||||||
|
prefetch={false}
|
||||||
className={cn(
|
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]",
|
"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]",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { getPublicCurrencies } from "@/api/currency";
|
import { getPublicCurrencies } from "@/api/currency";
|
||||||
import { getPlayerMe, getPlayerPing } from "@/api/player";
|
import { getPlayerMe, getPlayerPing } from "@/api/player";
|
||||||
|
import { isInIframe } from "@/components/iframe-bridge";
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -25,6 +26,8 @@ import { LotteryApiBizError } from "@/types/api/errors";
|
|||||||
|
|
||||||
const RETRY_ATTEMPTS = 3;
|
const RETRY_ATTEMPTS = 3;
|
||||||
const RETRY_DELAY_MS = 2000;
|
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() ?? "";
|
const MAIN_SITE_URL = process.env.NEXT_PUBLIC_MAIN_SITE_URL?.trim() ?? "";
|
||||||
|
|
||||||
@@ -134,11 +137,18 @@ export function EntryGate() {
|
|||||||
|
|
||||||
const doEntry = useCallback(async () => {
|
const doEntry = useCallback(async () => {
|
||||||
if (!effectiveToken) {
|
if (!effectiveToken) {
|
||||||
|
// 主站 iframe:token 由 MAIN_INIT_TOKEN 稍后到达,勿先闪「授权失败」
|
||||||
|
if (typeof window !== "undefined" && isInIframe() && !tokenFromUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPhase("failed");
|
setPhase("failed");
|
||||||
setFailureDetails([{ code: "NO_TOKEN", detailKey: "errors.noTokenDetail" }]);
|
setFailureDetails([{ code: "NO_TOKEN", detailKey: "errors.noTokenDetail" }]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPhase("loading");
|
||||||
|
setFailureDetails([]);
|
||||||
|
|
||||||
if (tokenFromUrl) {
|
if (tokenFromUrl) {
|
||||||
setBearerToken(tokenFromUrl);
|
setBearerToken(tokenFromUrl);
|
||||||
stripSearchParamFromBrowserUrl("token");
|
stripSearchParamFromBrowserUrl("token");
|
||||||
@@ -260,11 +270,32 @@ export function EntryGate() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionExpired) return;
|
if (sessionExpired) return;
|
||||||
|
|
||||||
|
const embedded = typeof window !== "undefined" && isInIframe() && !tokenFromUrl;
|
||||||
|
if (embedded && !effectiveToken) {
|
||||||
|
setPhase("loading");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tmr = window.setTimeout(() => {
|
const tmr = window.setTimeout(() => {
|
||||||
void doEntry();
|
void doEntry();
|
||||||
}, 300);
|
}, 300);
|
||||||
return () => window.clearTimeout(tmr);
|
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 (
|
return (
|
||||||
<div className="relative flex min-h-dvh flex-col bg-white">
|
<div className="relative flex min-h-dvh flex-col bg-white">
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
{data.previous_draw_no ? (
|
{data.previous_draw_no ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/results/${encodeURIComponent(data.previous_draw_no)}`}
|
href={`/results/${encodeURIComponent(data.previous_draw_no)}`}
|
||||||
|
prefetch={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline", size: "sm" }),
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
"min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",
|
"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 ? (
|
{data.next_draw_no ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/results/${encodeURIComponent(data.next_draw_no)}`}
|
href={`/results/${encodeURIComponent(data.next_draw_no)}`}
|
||||||
|
prefetch={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline", size: "sm" }),
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
"min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",
|
"min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export function DrawResultsListScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/results/${encodeURIComponent(featured.draw_no)}`}
|
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]"
|
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: "查看详情" })}
|
{t("results.openDetail", { defaultValue: "查看详情" })}
|
||||||
@@ -273,6 +274,7 @@ export function DrawResultsListScreen() {
|
|||||||
<Link
|
<Link
|
||||||
key={row.draw_no}
|
key={row.draw_no}
|
||||||
href={`/results/${encodeURIComponent(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]"
|
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">
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ type WalletLogsBlockProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 待对账 + 类型筛选 + 列表(供钱包主页与 `/wallet/logs` 共用) */
|
/** 类型筛选 + 列表(待对账见顶栏通知铃铛) */
|
||||||
export function WalletLogsBlock({
|
export function WalletLogsBlock({
|
||||||
logs,
|
logs,
|
||||||
logsLoading,
|
logsLoading,
|
||||||
@@ -74,29 +74,6 @@ export function WalletLogsBlock({
|
|||||||
|
|
||||||
return (
|
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">
|
<section className="space-y-3">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h2 className="text-sm font-black text-[#0b3f96]">{resolvedTitle}</h2>
|
<h2 className="text-sm font-black text-[#0b3f96]">{resolvedTitle}</h2>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getWalletLogs } from "@/api/wallet";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||||
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
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 { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||||
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
||||||
@@ -49,6 +50,7 @@ export function WalletLogsScreen() {
|
|||||||
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
||||||
: nextLogs,
|
: nextLogs,
|
||||||
);
|
);
|
||||||
|
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatWalletClientError(e, t));
|
setError(formatWalletClientError(e, t));
|
||||||
if (!append) {
|
if (!append) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TransferOutDialog,
|
TransferOutDialog,
|
||||||
} from "@/features/wallet/wallet-transfer-dialogs";
|
} from "@/features/wallet/wallet-transfer-dialogs";
|
||||||
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
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 { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||||
@@ -49,6 +50,7 @@ export function WalletScreen() {
|
|||||||
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
||||||
: nextLogs,
|
: nextLogs,
|
||||||
);
|
);
|
||||||
|
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
|
||||||
return nextLogs;
|
return nextLogs;
|
||||||
}, [currency, filter]);
|
}, [currency, filter]);
|
||||||
|
|
||||||
|
|||||||
72
src/hooks/use-pending-wallet-reconcile.ts
Normal file
72
src/hooks/use-pending-wallet-reconcile.ts
Normal file
@@ -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<void>;
|
||||||
|
} {
|
||||||
|
const bearerToken = usePlayerSessionStore((s) => s.bearerToken);
|
||||||
|
const [pending, setPending] = useState<WalletPendingTransfer[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const refresh = useCallback(async (): Promise<void> => {
|
||||||
|
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<WalletLogsRefreshDetail>).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<WalletLogsRefreshDetail>(WALLET_LOGS_REFRESH_EVENT, {
|
||||||
|
detail: { pending },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"wallet": "Wallet"
|
"wallet": "Wallet"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"empty": "No notifications"
|
||||||
|
},
|
||||||
"panel": {
|
"panel": {
|
||||||
"home": "Home"
|
"home": "Home"
|
||||||
},
|
},
|
||||||
@@ -367,6 +370,7 @@
|
|||||||
"flowsTitle": "Wallet logs",
|
"flowsTitle": "Wallet logs",
|
||||||
"totalRecords": "{{total}} records",
|
"totalRecords": "{{total}} records",
|
||||||
"emptyLogs": "No wallet logs",
|
"emptyLogs": "No wallet logs",
|
||||||
|
"noMoreLogs": "No more transactions",
|
||||||
"balanceAfter": "Balance after",
|
"balanceAfter": "Balance after",
|
||||||
"wsBalanceUpdated": "Balance {{change}} ({{reason}})",
|
"wsBalanceUpdated": "Balance {{change}} ({{reason}})",
|
||||||
"wsReason": {
|
"wsReason": {
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
"rules": "नियम",
|
"rules": "नियम",
|
||||||
"wallet": "वालेट"
|
"wallet": "वालेट"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"empty": "कुनै सूचना छैन"
|
||||||
|
},
|
||||||
"panel": {
|
"panel": {
|
||||||
"home": "गृह"
|
"home": "गृह"
|
||||||
},
|
},
|
||||||
@@ -367,6 +370,7 @@
|
|||||||
"flowsTitle": "वालेट लग",
|
"flowsTitle": "वालेट लग",
|
||||||
"totalRecords": "{{total}} रेकर्ड",
|
"totalRecords": "{{total}} रेकर्ड",
|
||||||
"emptyLogs": "वालेट लग छैन",
|
"emptyLogs": "वालेट लग छैन",
|
||||||
|
"noMoreLogs": "थप लेनदेन छैन",
|
||||||
"balanceAfter": "पछि बाँकी ब्यालेन्स",
|
"balanceAfter": "पछि बाँकी ब्यालेन्स",
|
||||||
"wsBalanceUpdated": "ब्यालेन्स {{change}} ({{reason}})",
|
"wsBalanceUpdated": "ब्यालेन्स {{change}} ({{reason}})",
|
||||||
"wsReason": {
|
"wsReason": {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"retryProgress": "正在重试({{current}}/{{total}})…"
|
"retryProgress": "正在重试({{current}}/{{total}})…"
|
||||||
},
|
},
|
||||||
"failure": {
|
"failure": {
|
||||||
"title": "授权失败111",
|
"title": "授权失败",
|
||||||
"subtitle": "无法完成授权,请重试。",
|
"subtitle": "无法完成授权,请重试。",
|
||||||
"detailsTitle": "失败详情",
|
"detailsTitle": "失败详情",
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
"rules": "规则",
|
"rules": "规则",
|
||||||
"wallet": "钱包"
|
"wallet": "钱包"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"empty": "暂无通知"
|
||||||
|
},
|
||||||
"panel": {
|
"panel": {
|
||||||
"home": "首页"
|
"home": "首页"
|
||||||
},
|
},
|
||||||
@@ -368,6 +371,7 @@
|
|||||||
"flowsTitle": "资金流水",
|
"flowsTitle": "资金流水",
|
||||||
"totalRecords": "共 {{total}} 条记录",
|
"totalRecords": "共 {{total}} 条记录",
|
||||||
"emptyLogs": "暂无流水",
|
"emptyLogs": "暂无流水",
|
||||||
|
"noMoreLogs": "没有更多流水",
|
||||||
"balanceAfter": "变更后余额",
|
"balanceAfter": "变更后余额",
|
||||||
"wsBalanceUpdated": "余额 {{change}}({{reason}})",
|
"wsBalanceUpdated": "余额 {{change}}({{reason}})",
|
||||||
"wsReason": {
|
"wsReason": {
|
||||||
|
|||||||
Reference in New Issue
Block a user