Some checks failed
lotteryfront CI / build (push) Has been cancelled
- 引入默认草稿行数常量及批量生成草稿行函数
- 根据玩法类别调整号码最大输入长度和清洗规则
- 更新号码输入组件以适应动态最大长度和格式规范
- 修正相关回调依赖,确保玩法类别变更时生效
- 改善行状态、占位符和样式传递,增强代码可读性
feat(results): 添加多提供商开奖结果切换功能
- 新增状态管理并展示多提供商按钮切换激活项
- 根据选中的提供商动态展示对应开奖结果数据
- 细化UI显示,支持多提供商名称并列渲染
feat(results): 在结果列表中显示多提供商名称
- 当存在多个提供商结果时展示其名称或代码
- 改善界面信息丰富度,方便用户辨识来源
feat(wallet): 支持账期还款释额类型的流水显示
- 调整账期流水类型文案,精准反映释额含义
- 引入账期时区参数以支持多时区时间格式化
- 自定义信用盘账期释额流水的颜色和文案显示
- 在流水明细和列表中根据时区格式化展示时间
feat(wallet): 支持钱包账期账单区间时区格式化
- 允许传入账期时区参数格式化账单区间时间
- 在账期账单区间显示组件中显示对应时区格式
- 根据玩家配置自动获取并传递账期时区信息
- 条件渲染中信用盘区块支持账期时区参数传递
chore(i18n): 更新中文钱包模块多处文案
- 新增“释额 {{amount}}”等账期释额相关文案
- 修改“账期收款”为“账期还款释额”以符合业务描述
refactor(types): 扩展开奖结果支持多提供商结构
- 新增DrawResultProviderRow类型定义以描述提供商结果
- 在DrawResultListItem中增加provider_results字段支持多结果
- 在玩家信息类型中新增settlement_timezone字段表示账期时区
595 lines
22 KiB
TypeScript
595 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import { ArrowDownLeft, ArrowUpRight, 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";
|
|
|
|
import { getWalletBalance, getWalletLogs, getWalletSettlementBills } from "@/api/wallet";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { PlayerPanel } from "@/components/layout/player-panel";
|
|
import {
|
|
TransferInDialog,
|
|
TransferOutDialog,
|
|
} from "@/features/wallet/wallet-transfer-dialogs";
|
|
import { PlayerMoneyDisplay } from "@/components/player-money-display";
|
|
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
|
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
|
import { formatMinorAsCurrency } from "@/lib/money";
|
|
import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime";
|
|
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
|
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
|
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
|
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
|
import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
|
|
import type {
|
|
WalletSettlementBillItem,
|
|
WalletSettlementBillsData,
|
|
} from "@/types/api/wallet-settlement-bills";
|
|
import type { PlayerMeData } from "@/types/api/player-me";
|
|
|
|
const WALLET_LOGS_PAGE_SIZE = 10;
|
|
type FundingModeView = "credit" | "wallet" | "unknown";
|
|
|
|
function scrollToWalletSection(id: string): void {
|
|
if (typeof document === "undefined") return;
|
|
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
|
|
function resolveFundingModeView(
|
|
balance: WalletBalanceData | null,
|
|
profile: PlayerMeData | null,
|
|
): FundingModeView {
|
|
if (isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile)) {
|
|
return "credit";
|
|
}
|
|
|
|
if (balance?.funding_mode === "wallet" || profile?.funding_mode === "wallet") {
|
|
return "wallet";
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
function settlementStatusLabel(
|
|
status: string,
|
|
t: (key: string, options?: { defaultValue?: string }) => string,
|
|
): string {
|
|
const fallback: Record<string, string> = {
|
|
pending_confirm: "待确认",
|
|
confirmed: "待收付",
|
|
partial_paid: "部分收付",
|
|
overdue: "已逾期",
|
|
settled: "已结清",
|
|
};
|
|
|
|
return t(`wallet.settlementStatus.${status}`, { defaultValue: fallback[status] ?? status });
|
|
}
|
|
|
|
function formatPeriodRange(
|
|
item: WalletSettlementBillItem,
|
|
settlementTimeZone?: string,
|
|
): string {
|
|
const start = item.period_start
|
|
? (settlementTimeZone
|
|
? formatLotteryInstantInTimeZone(item.period_start, settlementTimeZone)
|
|
: formatPlayerInstant(item.period_start)).slice(0, 10)
|
|
: "—";
|
|
const end = item.period_end
|
|
? (settlementTimeZone
|
|
? formatLotteryInstantInTimeZone(item.period_end, settlementTimeZone)
|
|
: formatPlayerInstant(item.period_end)).slice(0, 10)
|
|
: "—";
|
|
return `${start} / ${end}`;
|
|
}
|
|
|
|
function CreditSettlementBillsBlock({
|
|
data,
|
|
currency,
|
|
settlementTimeZone,
|
|
}: {
|
|
data: WalletSettlementBillsData | null;
|
|
currency: string;
|
|
settlementTimeZone?: string;
|
|
}) {
|
|
const { t } = useTranslation("player");
|
|
const summary = data?.summary;
|
|
const netPending = Number(summary?.net_pending ?? 0);
|
|
const pendingCount = Number(summary?.pending_count ?? 0);
|
|
const netDirection = netPending >= 0 ? "receivable" : "payable";
|
|
|
|
return (
|
|
<section className="rounded-2xl border border-[#dce7f7] bg-white px-3 py-3 shadow-[0_10px_28px_rgba(15,23,42,0.06)]">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<h2 className="text-sm font-black text-[#0b3f96]">
|
|
{t("wallet.settlementTitle", { defaultValue: "我的账期账单" })}
|
|
</h2>
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
{t("wallet.settlementHint", { defaultValue: "只显示未结清账单,中奖超出授信的部分在这里结算。" })}
|
|
</p>
|
|
</div>
|
|
<span className="rounded-full bg-[#f1f6ff] px-2.5 py-1 text-xs font-black text-[#32518d]">
|
|
{t("wallet.settlementPendingCount", { defaultValue: "{{count}}笔", count: pendingCount })}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
<div className="rounded-xl bg-emerald-50 px-3 py-2">
|
|
<p className="text-xs font-bold text-emerald-700">
|
|
{t("wallet.pendingReceivable", { defaultValue: "待收" })}
|
|
</p>
|
|
<p className="mt-1 font-mono text-sm font-black tabular-nums text-emerald-700">
|
|
{formatMinorAsCurrency(summary?.pending_receivable ?? 0, currency)}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-xl bg-red-50 px-3 py-2">
|
|
<p className="text-xs font-bold text-red-700">
|
|
{t("wallet.pendingPayable", { defaultValue: "待付" })}
|
|
</p>
|
|
<p className="mt-1 font-mono text-sm font-black tabular-nums text-red-700">
|
|
{formatMinorAsCurrency(summary?.pending_payable ?? 0, currency)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-2 rounded-xl bg-[#f8fbff] px-3 py-2 text-xs">
|
|
<span className="font-semibold text-slate-500">
|
|
{netDirection === "receivable"
|
|
? t("wallet.netReceivable", { defaultValue: "净待收" })
|
|
: t("wallet.netPayable", { defaultValue: "净待付" })}
|
|
</span>
|
|
<span className={netDirection === "receivable" ? "ml-2 font-mono font-black text-emerald-700" : "ml-2 font-mono font-black text-red-700"}>
|
|
{formatMinorAsCurrency(Math.abs(netPending), currency)}
|
|
</span>
|
|
</div>
|
|
|
|
{data === null ? (
|
|
<Skeleton className="mt-3 h-20 w-full rounded-xl" />
|
|
) : data.items.length === 0 ? (
|
|
<p className="mt-3 rounded-xl border border-dashed border-[#dce7f7] py-5 text-center text-sm text-slate-500">
|
|
{t("wallet.noSettlementBills", { defaultValue: "暂无待结算账单" })}
|
|
</p>
|
|
) : (
|
|
<ul className="mt-3 space-y-2">
|
|
{data.items.map((item) => {
|
|
const receivable = item.direction === "receivable";
|
|
return (
|
|
<li key={item.id} className="rounded-xl border border-[#e1eaf6] px-3 py-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span className={receivable ? "rounded-full bg-emerald-100 p-1 text-emerald-700" : "rounded-full bg-red-100 p-1 text-red-700"}>
|
|
{receivable ? <ArrowDownLeft className="size-3.5" /> : <ArrowUpRight className="size-3.5" />}
|
|
</span>
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-black text-[#101a33]">
|
|
{receivable
|
|
? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" })
|
|
: t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })}
|
|
</p>
|
|
<p className="mt-0.5 text-[11px] text-slate-500">
|
|
{formatPeriodRange(item, settlementTimeZone)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="shrink-0 text-right">
|
|
<p className={receivable ? "font-mono text-sm font-black text-emerald-700" : "font-mono text-sm font-black text-red-700"}>
|
|
{formatMinorAsCurrency(item.unpaid_amount, currency)}
|
|
</p>
|
|
<p className="mt-0.5 text-[11px] text-slate-500">{settlementStatusLabel(item.status, t)}</p>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
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 [settlementBills, setSettlementBills] = useState<WalletSettlementBillsData | 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 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 settlementTimeZone = profile?.settlement_timezone || "UTC";
|
|
const fundingModeView = resolveFundingModeView(balance, profile);
|
|
const fundingModeKnown = fundingModeView !== "unknown";
|
|
const isCreditPlayer = fundingModeView === "credit";
|
|
|
|
useEffect(() => {
|
|
if (actionDeepLinkHandledRef.current || loading) return;
|
|
const action = searchParams.get("action");
|
|
if (action !== "transfer-in" && action !== "transfer-out") return;
|
|
|
|
actionDeepLinkHandledRef.current = true;
|
|
if (!isCreditPlayer) {
|
|
queueMicrotask(() => {
|
|
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 === "pending" ? "wallet-pending" : "wallet-logs";
|
|
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({
|
|
page: targetPage,
|
|
size: WALLET_LOGS_PAGE_SIZE,
|
|
type: filter || undefined,
|
|
currency,
|
|
});
|
|
setLogs((current) =>
|
|
append && current
|
|
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
|
: nextLogs,
|
|
);
|
|
return nextLogs;
|
|
}, [currency, filter]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
void (async () => {
|
|
setError(null);
|
|
setLoading(true);
|
|
try {
|
|
const [b, nextLogs] = await Promise.all([
|
|
getWalletBalance({ currency }),
|
|
getWalletLogs({
|
|
page: 1,
|
|
size: WALLET_LOGS_PAGE_SIZE,
|
|
type: filter || undefined,
|
|
currency,
|
|
}),
|
|
]);
|
|
if (cancelled) return;
|
|
setBalance(b);
|
|
setLogs(nextLogs);
|
|
if (isCreditFundingPlayer(b) || isCreditFundingPlayer(profile)) {
|
|
const bills = await getWalletSettlementBills();
|
|
if (!cancelled) setSettlementBills(bills);
|
|
} else {
|
|
setSettlementBills(null);
|
|
}
|
|
} catch (e) {
|
|
if (!cancelled) {
|
|
setError(formatWalletClientError(e, t));
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
setLoading(false);
|
|
setLogsLoading(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [currency, t]); // eslint-disable-line react-hooks/exhaustive-deps -- 币种切换整页刷新;流水筛选见下方 effect
|
|
|
|
useEffect(() => {
|
|
if (!filterInitializedRef.current) {
|
|
filterInitializedRef.current = true;
|
|
prevFilterRef.current = filter;
|
|
return;
|
|
}
|
|
if (prevFilterRef.current === filter) {
|
|
return;
|
|
}
|
|
prevFilterRef.current = filter;
|
|
|
|
let cancelled = false;
|
|
setError(null);
|
|
setLogsLoading(true);
|
|
|
|
void (async () => {
|
|
try {
|
|
const nextLogs = await getWalletLogs({
|
|
page: 1,
|
|
size: WALLET_LOGS_PAGE_SIZE,
|
|
type: filter || undefined,
|
|
currency,
|
|
});
|
|
if (!cancelled) {
|
|
setLogs(nextLogs);
|
|
}
|
|
} catch (e) {
|
|
if (!cancelled) {
|
|
setError(formatWalletClientError(e, t));
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
setLogsLoading(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [currency, filter, t]);
|
|
|
|
const handleFilterChange = useCallback((value: string) => {
|
|
if (value === filter) {
|
|
return;
|
|
}
|
|
|
|
setLogs(null);
|
|
setLogsLoading(true);
|
|
setFilter(value);
|
|
}, [filter]);
|
|
|
|
const refreshAll = useCallback(async () => {
|
|
setError(null);
|
|
setLogsLoading(true);
|
|
try {
|
|
// 并行请求余额和日志
|
|
const [b] = await Promise.all([
|
|
getWalletBalance({ currency }),
|
|
loadLogs(1, false),
|
|
]);
|
|
setBalance(b);
|
|
if (isCreditFundingPlayer(b) || isCreditFundingPlayer(profile)) {
|
|
setSettlementBills(await getWalletSettlementBills());
|
|
} else {
|
|
setSettlementBills(null);
|
|
}
|
|
} catch (e) {
|
|
setError(formatWalletClientError(e, t));
|
|
} finally {
|
|
setLogsLoading(false);
|
|
setLoading(false);
|
|
}
|
|
}, [currency, loadLogs, profile, t]);
|
|
|
|
useEffect(() => {
|
|
const onCurrencyChange = () => void refreshAll();
|
|
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
|
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
|
}, [refreshAll]);
|
|
|
|
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
|
|
|
|
const displayMinor = isCreditPlayer
|
|
? Number(balance?.available_balance ?? 0)
|
|
: Number(balance?.balance ?? 0);
|
|
const panelTitle = !fundingModeKnown
|
|
? t("wallet.loadingTitle", { defaultValue: "账户资金" })
|
|
: isCreditPlayer
|
|
? t("wallet.creditTitle", { defaultValue: "信用" })
|
|
: t("wallet.title");
|
|
|
|
const loadMore = useCallback(() => {
|
|
if (!logs || !hasMore || loadingMore) return;
|
|
|
|
setError(null);
|
|
setLoadingMore(true);
|
|
void loadLogs(logs.page + 1, true)
|
|
.catch((e) => {
|
|
setError(formatWalletClientError(e, t));
|
|
})
|
|
.finally(() => {
|
|
setLoadingMore(false);
|
|
});
|
|
}, [hasMore, loadLogs, loadingMore, logs, t]);
|
|
|
|
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={panelTitle}>
|
|
<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 refreshAll()}
|
|
>
|
|
{t("actions.retry")}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
|
|
<section className="relative overflow-hidden rounded-xl bg-[#e5002c] px-3 py-4 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]">
|
|
<Image
|
|
src="/entry/image5.png"
|
|
alt=""
|
|
fill
|
|
className="pointer-events-none object-cover object-center"
|
|
aria-hidden
|
|
/>
|
|
<div className="relative flex items-center gap-3">
|
|
<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">
|
|
<p className="text-sm font-semibold text-white/90">
|
|
{fundingModeKnown && isCreditPlayer
|
|
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
|
|
: fundingModeKnown
|
|
? t("wallet.balance")
|
|
: t("wallet.loadingBalance", { defaultValue: "正在加载" })}
|
|
</p>
|
|
{loading || !fundingModeKnown ? (
|
|
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
|
|
) : (
|
|
<PlayerMoneyDisplay
|
|
amountMinor={displayMinor}
|
|
currency={currency}
|
|
className="mt-1 text-white"
|
|
/>
|
|
)}
|
|
<p className="mt-2 text-xs text-white/75">
|
|
{!fundingModeKnown
|
|
? t("wallet.loadingAccountMode", { defaultValue: "正在确认资金模式…" })
|
|
: isCreditPlayer
|
|
? t("wallet.creditSummary", {
|
|
defaultValue: "授信 {{limit}} · 已用 {{used}}",
|
|
limit: formatMinorAsCurrency(balance?.credit_limit ?? 0, currency),
|
|
used: formatMinorAsCurrency(balance?.used_credit ?? 0, currency),
|
|
})
|
|
: t("wallet.available", {
|
|
amount: formatMinorAsCurrency(balance?.available_balance ?? 0, currency),
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{fundingModeKnown && isCreditPlayer ? (
|
|
<CreditSettlementBillsBlock
|
|
data={settlementBills}
|
|
currency={currency}
|
|
settlementTimeZone={settlementTimeZone}
|
|
/>
|
|
) : null}
|
|
|
|
{fundingModeKnown && !isCreditPlayer ? (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<TransferInDialog
|
|
idPrefix="wallet-"
|
|
currency={currency}
|
|
lotteryMinor={Number(balance?.balance ?? 0)}
|
|
mainMinor={
|
|
balance?.main_balance === null || balance?.main_balance === undefined
|
|
? null
|
|
: Number(balance.main_balance)
|
|
}
|
|
onSuccess={refreshAll}
|
|
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-"
|
|
currency={currency}
|
|
availableMinor={Number(balance?.available_balance ?? 0)}
|
|
onSuccess={refreshAll}
|
|
triggerVariant="hall"
|
|
triggerLabel={t("wallet.transferOut", { defaultValue: "Transfer Out" })}
|
|
triggerClassName="h-14 rounded-2xl text-base font-black"
|
|
open={transferOutOpen}
|
|
onOpenChange={setTransferOutOpen}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
{fundingModeKnown && !isCreditPlayer && (logs?.pending_reconcile?.length ?? 0) > 0 ? (
|
|
<section
|
|
id="wallet-pending"
|
|
className="scroll-mt-3 rounded-2xl border border-amber-200 bg-amber-50 px-3 py-3 text-sm text-amber-900"
|
|
>
|
|
<h2 className="text-sm font-black text-amber-800">
|
|
{t("wallet.pendingSectionTitle", { defaultValue: "待对账划转" })}
|
|
</h2>
|
|
<p className="mt-1 text-xs text-amber-700">
|
|
{t("wallet.pendingSectionHint", {
|
|
defaultValue: "以下划转仍在与主站对账,请稍后刷新查看结果。",
|
|
})}
|
|
</p>
|
|
<ul className="mt-3 space-y-2">
|
|
{logs?.pending_reconcile.map((item) => (
|
|
<li
|
|
key={item.transfer_no}
|
|
className="rounded-xl border border-amber-200 bg-white px-3 py-2"
|
|
>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="font-semibold">
|
|
{item.direction === "in"
|
|
? t("wallet.flow.transfer_in", { defaultValue: "转入" })
|
|
: t("wallet.flow.transfer_out", { defaultValue: "转出" })}
|
|
</span>
|
|
<span className="font-black tabular-nums text-amber-800">
|
|
{formatMinorAsCurrency(item.amount, item.currency_code || currency)}
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 font-mono text-[11px] text-amber-600">{item.transfer_no}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
) : null}
|
|
|
|
<div id="wallet-logs" className="scroll-mt-3">
|
|
{fundingModeKnown ? (
|
|
<WalletLogsBlock
|
|
creditMode={isCreditPlayer}
|
|
logs={logs}
|
|
logsLoading={loading || logsLoading}
|
|
loadingMore={loadingMore}
|
|
hasMore={hasMore}
|
|
onLoadMore={loadMore}
|
|
loadMoreRef={loadMoreRef}
|
|
filter={filter}
|
|
onFilterChange={handleFilterChange}
|
|
currency={currency}
|
|
settlementTimeZone={isCreditPlayer ? settlementTimeZone : undefined}
|
|
/>
|
|
) : (
|
|
<Skeleton className="h-40 w-full rounded-lg" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</PlayerPanel>
|
|
);
|
|
}
|