- 合并钱包、订单、开奖、通知等独立 screen 到页面层,减少重复壳组件 - 优化订单列表/详情、钱包划转与待对账展示、开奖核对与结果详情交互 - 改进入口 gate、移动端视口与下拉刷新在窄屏下的表现 - dev 绑定 0.0.0.0 并默认放行 192.168/10 网段,修复局域网 HMR WebSocket
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { TransferInPage } from "@/features/wallet/wallet-transfer-forms";
|
||||
import { WalletTransferCreditGuard } from "@/features/wallet/wallet-transfer-credit-guard";
|
||||
import { WalletTransferLoadingPanel } from "@/features/wallet/wallet-transfer-loading-panel";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { useApiQuery } from "@/hooks/use-api-query";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
|
||||
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
|
||||
|
||||
/** 独立路由 `/wallet/transfer-in` */
|
||||
export function TransferInScreen() {
|
||||
const router = useRouter();
|
||||
const { activeCurrency: currency } = useActivePlayerCurrency();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { data: balance, isLoading: loading } = useApiQuery(
|
||||
BALANCE_KEY(currency),
|
||||
() => getWalletBalance({ currency }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onRefresh = () => void mutate(BALANCE_KEY(currency));
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
}, [mutate, currency]);
|
||||
|
||||
const onSuccess = useCallback(async () => {
|
||||
await mutate(BALANCE_KEY(currency));
|
||||
router.push("/wallet");
|
||||
}, [mutate, currency, router]);
|
||||
|
||||
if (loading && !balance) {
|
||||
return <WalletTransferLoadingPanel titleKey="wallet.transferInTitle" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WalletTransferCreditGuard balance={balance} loading={loading}>
|
||||
<TransferInPage
|
||||
currency={currency}
|
||||
lotteryMinor={Number(balance?.balance ?? 0)}
|
||||
mainMinor={
|
||||
balance?.main_balance === null || balance?.main_balance === undefined
|
||||
? null
|
||||
: Number(balance.main_balance)
|
||||
}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</WalletTransferCreditGuard>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { TransferOutPage } from "@/features/wallet/wallet-transfer-forms";
|
||||
import { WalletTransferCreditGuard } from "@/features/wallet/wallet-transfer-credit-guard";
|
||||
import { WalletTransferLoadingPanel } from "@/features/wallet/wallet-transfer-loading-panel";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { useApiQuery } from "@/hooks/use-api-query";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
|
||||
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
|
||||
|
||||
/** 独立路由 `/wallet/transfer-out` */
|
||||
export function TransferOutScreen() {
|
||||
const router = useRouter();
|
||||
const { activeCurrency: currency } = useActivePlayerCurrency();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { data: balance, isLoading: loading } = useApiQuery(
|
||||
BALANCE_KEY(currency),
|
||||
() => getWalletBalance({ currency }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onRefresh = () => void mutate(BALANCE_KEY(currency));
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
}, [mutate, currency]);
|
||||
|
||||
const onSuccess = useCallback(async () => {
|
||||
await mutate(BALANCE_KEY(currency));
|
||||
router.push("/wallet");
|
||||
}, [mutate, currency, router]);
|
||||
|
||||
if (loading && !balance) {
|
||||
return <WalletTransferLoadingPanel titleKey="wallet.transferOutTitle" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WalletTransferCreditGuard balance={balance} loading={loading}>
|
||||
<TransferOutPage
|
||||
currency={currency}
|
||||
availableMinor={Number(balance?.available_balance ?? 0)}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</WalletTransferCreditGuard>
|
||||
);
|
||||
}
|
||||
@@ -241,24 +241,14 @@ export function LogRow({
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between rounded-xl bg-[#f8fbff] px-3 py-2 text-xs">
|
||||
{creditMode && item.affects_available_credit === false ? (
|
||||
<span className="font-medium text-slate-500">
|
||||
{t("wallet.creditPaymentRecordOnly", {
|
||||
defaultValue: "账期收付记账,不计入可用信用",
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-semibold text-slate-500">
|
||||
{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}
|
||||
</span>
|
||||
<span className="font-mono font-black tabular-nums text-[#32518d]">
|
||||
{item.balance_after != null
|
||||
? formatMinorAsCurrency(item.balance_after, ccy)
|
||||
: "—"}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="font-semibold text-slate-500">
|
||||
{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}
|
||||
</span>
|
||||
<span className="font-mono font-black tabular-nums text-[#32518d]">
|
||||
{item.balance_after != null
|
||||
? formatMinorAsCurrency(item.balance_after, ccy)
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getWalletBalance, 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 { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
|
||||
|
||||
const WALLET_LOGS_PAGE_SIZE = 10;
|
||||
|
||||
export function WalletLogsScreen() {
|
||||
const { activeCurrency: currency } = useActivePlayerCurrency();
|
||||
const { t } = useTranslation("player");
|
||||
const [logs, setLogs] = useState<WalletLogsData | null>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const profile = usePlayerSessionStore((s) => s.profile);
|
||||
const [creditMode, setCreditMode] = useState(() => isCreditFundingPlayer(profile));
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const fetchPassRef = useRef(true);
|
||||
|
||||
const load = useCallback(async (targetPage = 1, append = false) => {
|
||||
setError(null);
|
||||
if (append) {
|
||||
setLoadingMore(true);
|
||||
} else if (fetchPassRef.current) {
|
||||
setLoading(true);
|
||||
fetchPassRef.current = false;
|
||||
} else {
|
||||
setLogsLoading(true);
|
||||
}
|
||||
try {
|
||||
const balance = await getWalletBalance({ currency });
|
||||
setCreditMode(isCreditFundingPlayer(balance));
|
||||
const nextLogs = await getWalletLogs({
|
||||
page: targetPage,
|
||||
size: WALLET_LOGS_PAGE_SIZE,
|
||||
type: filter || undefined,
|
||||
currency,
|
||||
});
|
||||
setLogs((current) =>
|
||||
append && current
|
||||
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
||||
: nextLogs,
|
||||
);
|
||||
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
|
||||
} catch (e) {
|
||||
setError(formatWalletClientError(e, t));
|
||||
if (!append) {
|
||||
setLogs(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLogsLoading(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [currency, filter, t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void load(1, false);
|
||||
});
|
||||
}, [currency, load]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => void load(1, false);
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
}, [load]);
|
||||
|
||||
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!logs || !hasMore || loadingMore) return;
|
||||
void load(logs.page + 1, true);
|
||||
}, [hasMore, load, loadingMore, logs]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = loadMoreRef.current;
|
||||
if (!target || loading || logsLoading || loadingMore || !hasMore) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "160px" },
|
||||
);
|
||||
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, loadMore, loading, loadingMore, logsLoading]);
|
||||
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={
|
||||
creditMode
|
||||
? t("wallet.creditLogsTitle", { defaultValue: "信用流水" })
|
||||
: t("wallet.logsTitle")
|
||||
}
|
||||
backHref="/wallet"
|
||||
backLabel={
|
||||
creditMode
|
||||
? t("wallet.creditTitle", { defaultValue: "信用" })
|
||||
: t("wallet.title")
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{error ? (
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-700">
|
||||
<p>{error}</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
|
||||
onClick={() => void load()}
|
||||
>
|
||||
{t("actions.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<WalletLogsBlock
|
||||
logs={logs}
|
||||
logsLoading={loading || logsLoading}
|
||||
loadingMore={loadingMore}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadMoreRef={loadMoreRef}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
currency={currency}
|
||||
creditMode={creditMode}
|
||||
/>
|
||||
</div>
|
||||
</PlayerPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
|
||||
|
||||
/** 钱包盘:顶栏铃铛隐藏后,在钱包页展示待对账提醒入口 */
|
||||
export function WalletPendingReconcileBanner() {
|
||||
const { t } = useTranslation("player");
|
||||
const { pending, unreadCount, hasPending } = usePendingWalletReconcile();
|
||||
|
||||
if (!hasPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-3 text-sm text-amber-950">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600" aria-hidden />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-bold text-amber-900">{t("wallet.pendingTitle")}</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-amber-950/85">
|
||||
{t("wallet.pendingDescription")}
|
||||
</p>
|
||||
{unreadCount > 0 ? (
|
||||
<p className="mt-1.5 text-xs font-semibold text-amber-800">
|
||||
{t("notifications.unreadCount", { count: unreadCount })}
|
||||
</p>
|
||||
) : null}
|
||||
<Link
|
||||
href="/notifications"
|
||||
className="mt-2 inline-flex text-xs font-bold text-[#0b56b7] underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("wallet.viewPendingReconcile", {
|
||||
defaultValue: "查看待对账详情({{count}})",
|
||||
count: pending.length,
|
||||
})}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
src/features/wallet/wallet-pending-reconcile-section.tsx
Normal file
153
src/features/wallet/wallet-pending-reconcile-section.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { BellRing, CheckCheck } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
|
||||
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||
import {
|
||||
pendingReconcileDescriptionKey,
|
||||
pendingReconcileTitleKey,
|
||||
} from "@/lib/pending-reconcile-notification";
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type WalletPendingReconcileSectionProps = {
|
||||
onViewLogs?: () => void;
|
||||
};
|
||||
|
||||
/** 钱包页内嵌:待对账提醒(原独立 /notifications 页) */
|
||||
export function WalletPendingReconcileSection({
|
||||
onViewLogs,
|
||||
}: WalletPendingReconcileSectionProps) {
|
||||
const { t } = useTranslation("player");
|
||||
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
|
||||
const { pending, unreadPending, unreadCount, loading, markAsRead, markAllAsRead } =
|
||||
usePendingWalletReconcile();
|
||||
const unreadSet = new Set(unreadPending.map((item) => item.transfer_no));
|
||||
|
||||
if (creditMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="wallet-pending" className="scroll-mt-3 space-y-3">
|
||||
<div className="flex items-center justify-between rounded-xl border border-[#dce7f7] bg-[#f8fbff] px-3 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<BellRing className="size-4 text-[#0b56b7]" aria-hidden />
|
||||
<p className="text-sm font-semibold text-[#0b3f96]">
|
||||
{t("notifications.title")}
|
||||
{unreadCount > 0 ? (
|
||||
<span className="ml-1.5 text-xs font-bold text-amber-700">
|
||||
({t("notifications.unreadCount", { count: unreadCount })})
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 px-2 text-xs font-bold text-[#0b56b7] hover:bg-[#ebf2ff]"
|
||||
onClick={markAllAsRead}
|
||||
disabled={pending.length === 0 || unreadCount === 0}
|
||||
>
|
||||
<CheckCheck className="mr-1 size-3.5" aria-hidden />
|
||||
{t("notifications.markAllRead")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading && pending.length === 0 ? (
|
||||
<div className="rounded-xl border border-[#dce7f7] bg-white px-4 py-6 text-center text-sm text-slate-500">
|
||||
{t("actions.loading")}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading && pending.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-white px-4 py-8 text-center">
|
||||
<p className="text-sm text-slate-500">{t("notifications.empty")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{pending.length > 0 ? (
|
||||
<ul className="space-y-2">
|
||||
{pending.map((item) => {
|
||||
const cardRead = !unreadSet.has(item.transfer_no);
|
||||
return (
|
||||
<li
|
||||
key={item.transfer_no}
|
||||
className={cn(
|
||||
"rounded-xl border px-3 py-3 transition-colors",
|
||||
cardRead
|
||||
? "border-[#e4eaf4] bg-white"
|
||||
: "border-amber-200 bg-amber-50/80",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold text-amber-900">
|
||||
{t(pendingReconcileTitleKey(item.type))}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-slate-500">
|
||||
{formatPlayerInstant(item.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-bold text-amber-800">
|
||||
{t("notifications.pendingBadge")}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[11px] font-semibold",
|
||||
cardRead ? "text-slate-400" : "text-amber-700",
|
||||
)}
|
||||
>
|
||||
{cardRead ? t("notifications.read") : t("notifications.unread")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-xs leading-relaxed text-amber-950/85">
|
||||
{t(pendingReconcileDescriptionKey(item.type))}
|
||||
</p>
|
||||
|
||||
<p className="mt-2 text-sm text-slate-700">
|
||||
<span className="text-xs font-medium text-slate-500">
|
||||
{t("notifications.amountLabel")}{" "}
|
||||
</span>
|
||||
{formatMinorAsCurrency(item.amount, item.currency_code)}
|
||||
</p>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 rounded-full border-[#dce7f7] px-3 text-xs font-semibold text-[#0b56b7]"
|
||||
onClick={() => markAsRead(item.transfer_no)}
|
||||
>
|
||||
{t("notifications.markRead")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-8 rounded-full bg-[#07459f] px-3 text-xs font-semibold text-white hover:bg-[#063b88]"
|
||||
onClick={() => {
|
||||
markAsRead(item.transfer_no);
|
||||
onViewLogs?.();
|
||||
}}
|
||||
>
|
||||
{t("notifications.viewLogs")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Wallet } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
TransferInDialog,
|
||||
TransferOutDialog,
|
||||
} from "@/features/wallet/wallet-transfer-dialogs";
|
||||
import { WalletPendingReconcileBanner } from "@/features/wallet/wallet-pending-reconcile-banner";
|
||||
import { WalletPendingReconcileSection } from "@/features/wallet/wallet-pending-reconcile-section";
|
||||
import { PlayerMoneyDisplay } from "@/components/player-money-display";
|
||||
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
||||
import { dispatchWalletLogsRefresh } from "@/hooks/use-pending-wallet-reconcile";
|
||||
@@ -28,9 +29,18 @@ import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-l
|
||||
|
||||
const WALLET_LOGS_PAGE_SIZE = 10;
|
||||
|
||||
function scrollToWalletSection(id: string): void {
|
||||
if (typeof document === "undefined") return;
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
export function WalletScreen() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { activeCurrency: currency } = useActivePlayerCurrency();
|
||||
const { t } = useTranslation("player");
|
||||
const [transferInOpen, setTransferInOpen] = useState(false);
|
||||
const [transferOutOpen, setTransferOutOpen] = useState(false);
|
||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||
const [logs, setLogs] = useState<WalletLogsData | null>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
@@ -39,8 +49,47 @@ export function WalletScreen() {
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const filterInitializedRef = useRef(false);
|
||||
const prevFilterRef = useRef("");
|
||||
const actionDeepLinkHandledRef = useRef(false);
|
||||
const sectionDeepLinkHandledRef = useRef(false);
|
||||
|
||||
const profile = usePlayerSessionStore((s) => s.profile);
|
||||
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionDeepLinkHandledRef.current || loading) return;
|
||||
const action = searchParams.get("action");
|
||||
if (action !== "transfer-in" && action !== "transfer-out") return;
|
||||
|
||||
actionDeepLinkHandledRef.current = true;
|
||||
if (!isCreditPlayer) {
|
||||
if (action === "transfer-in") {
|
||||
setTransferInOpen(true);
|
||||
} else {
|
||||
setTransferOutOpen(true);
|
||||
}
|
||||
}
|
||||
router.replace("/wallet", { scroll: false });
|
||||
}, [isCreditPlayer, loading, router, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sectionDeepLinkHandledRef.current || loading) return;
|
||||
const section = searchParams.get("section");
|
||||
if (section !== "logs" && section !== "pending") return;
|
||||
|
||||
sectionDeepLinkHandledRef.current = true;
|
||||
const targetId =
|
||||
section === "logs" || (section === "pending" && isCreditPlayer)
|
||||
? "wallet-logs"
|
||||
: "wallet-pending";
|
||||
const timer = window.setTimeout(() => {
|
||||
scrollToWalletSection(targetId);
|
||||
router.replace("/wallet", { scroll: false });
|
||||
}, 120);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [isCreditPlayer, loading, router, searchParams]);
|
||||
|
||||
const loadLogs = useCallback(async (targetPage = 1, append = false) => {
|
||||
const nextLogs = await getWalletLogs({
|
||||
@@ -164,8 +213,6 @@ export function WalletScreen() {
|
||||
|
||||
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
|
||||
|
||||
const profile = usePlayerSessionStore((s) => s.profile);
|
||||
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
|
||||
const displayMinor = isCreditPlayer
|
||||
? Number(balance?.available_balance ?? 0)
|
||||
: Number(balance?.balance ?? 0);
|
||||
@@ -261,17 +308,8 @@ export function WalletScreen() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{isCreditPlayer ? (
|
||||
<p className="rounded-xl border border-[#d6e4ff] bg-[#f5f9ff] px-3 py-3 text-sm text-[#0b3f96]/85">
|
||||
{t("wallet.creditNoTransferHint", {
|
||||
defaultValue:
|
||||
"由代理授信,无需主站转入转出;中奖不即时派彩,盈亏在账期统一结算,额度调整请联系代理。",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<WalletPendingReconcileBanner />
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{!isCreditPlayer ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<TransferInDialog
|
||||
idPrefix="wallet-"
|
||||
currency={currency}
|
||||
@@ -285,6 +323,8 @@ export function WalletScreen() {
|
||||
triggerVariant="hall"
|
||||
triggerLabel={t("wallet.transferIn", { defaultValue: "Transfer In" })}
|
||||
triggerClassName="h-14 rounded-2xl text-base font-black"
|
||||
open={transferInOpen}
|
||||
onOpenChange={setTransferInOpen}
|
||||
/>
|
||||
<TransferOutDialog
|
||||
idPrefix="wallet-"
|
||||
@@ -294,11 +334,17 @@ export function WalletScreen() {
|
||||
triggerVariant="hall"
|
||||
triggerLabel={t("wallet.transferOut", { defaultValue: "Transfer Out" })}
|
||||
triggerClassName="h-14 rounded-2xl text-base font-black"
|
||||
open={transferOutOpen}
|
||||
onOpenChange={setTransferOutOpen}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<WalletPendingReconcileSection
|
||||
onViewLogs={() => scrollToWalletSection("wallet-logs")}
|
||||
/>
|
||||
|
||||
<div id="wallet-logs" className="scroll-mt-3">
|
||||
<WalletLogsBlock
|
||||
creditMode={isCreditPlayer}
|
||||
logs={logs}
|
||||
@@ -311,6 +357,7 @@ export function WalletScreen() {
|
||||
onFilterChange={setFilter}
|
||||
currency={currency}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PlayerPanel>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||
|
||||
@@ -21,7 +18,6 @@ export function WalletTransferCreditGuard({
|
||||
children,
|
||||
}: WalletTransferCreditGuardProps) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation("player");
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !balance) {
|
||||
@@ -37,20 +33,7 @@ export function WalletTransferCreditGuard({
|
||||
}
|
||||
|
||||
if (isCreditFundingPlayer(balance)) {
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={t("wallet.creditTitle", { defaultValue: "信用" })}
|
||||
backHref="/wallet"
|
||||
backLabel={t("wallet.creditTitle", { defaultValue: "信用" })}
|
||||
>
|
||||
<p className="rounded-xl border border-[#d6e4ff] bg-[#f5f9ff] px-3 py-3 text-sm text-[#0b3f96]/85">
|
||||
{t("wallet.creditNoTransferHint", {
|
||||
defaultValue:
|
||||
"由代理授信,无需主站转入转出;中奖不即时派彩,盈亏在账期统一结算,额度调整请联系代理。",
|
||||
})}
|
||||
</p>
|
||||
</PlayerPanel>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -23,6 +23,8 @@ type BaseProps = {
|
||||
onSuccess: () => Promise<void>;
|
||||
/** 避免同页多实例 input id 冲突 */
|
||||
idPrefix?: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export function TransferInDialog({
|
||||
@@ -34,6 +36,8 @@ export function TransferInDialog({
|
||||
triggerClassName,
|
||||
triggerVariant = "wallet",
|
||||
triggerLabel,
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
}: BaseProps & {
|
||||
lotteryMinor: number;
|
||||
mainMinor?: number | null;
|
||||
@@ -41,7 +45,19 @@ export function TransferInDialog({
|
||||
triggerVariant?: "wallet" | "hall";
|
||||
triggerLabel?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
||||
const setOpen = useCallback(
|
||||
(next: boolean) => {
|
||||
if (isControlled) {
|
||||
controlledOnOpenChange?.(next);
|
||||
} else {
|
||||
setUncontrolledOpen(next);
|
||||
}
|
||||
},
|
||||
[controlledOnOpenChange, isControlled],
|
||||
);
|
||||
const { t } = useTranslation("player");
|
||||
const resolvedTriggerLabel = triggerLabel ?? t("wallet.transferIn");
|
||||
|
||||
@@ -94,13 +110,27 @@ export function TransferOutDialog({
|
||||
triggerClassName,
|
||||
triggerVariant = "wallet",
|
||||
triggerLabel,
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
}: BaseProps & {
|
||||
availableMinor: number;
|
||||
triggerClassName?: string;
|
||||
triggerVariant?: "wallet" | "hall";
|
||||
triggerLabel?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
||||
const setOpen = useCallback(
|
||||
(next: boolean) => {
|
||||
if (isControlled) {
|
||||
controlledOnOpenChange?.(next);
|
||||
} else {
|
||||
setUncontrolledOpen(next);
|
||||
}
|
||||
},
|
||||
[controlledOnOpenChange, isControlled],
|
||||
);
|
||||
const { t } = useTranslation("player");
|
||||
const resolvedTriggerLabel = triggerLabel ?? t("wallet.transferOut");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user