- 新增接口 getWalletSettlementBills 获取信用盘玩家账期账单数据 - 在钱包日志块中添加业务类型标签支持,增强日志类型显示 - WalletScreen 中增加账期账单数据的加载和展示逻辑 - 新增 CreditSettlementBillsBlock 组件用于展示账期账单摘要和详情列表 - 支持账单状态显示和账期区间格式化 - 仅信用盘玩家显示账期账单模块,其他类型玩家隐藏 - 新增 wallet-settlement-bills.ts 类型定义文件,完善账期账单数据结构
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
||||
WalletTransferResultData,
|
||||
} from "@/types/api/wallet-transfer";
|
||||
import type { GetWalletLogsParams, WalletLogsData } from "@/types/api/wallet-logs";
|
||||
import type { WalletSettlementBillsData } from "@/types/api/wallet-settlement-bills";
|
||||
|
||||
export type GetWalletBalanceParams = {
|
||||
/** Query `currency`,不传则用玩家默认币种 */
|
||||
@@ -31,6 +32,13 @@ export function getWalletLogs(
|
||||
);
|
||||
}
|
||||
|
||||
/** `GET /api/v1/wallet/settlement-bills`(信用盘玩家待收/待付账单) */
|
||||
export function getWalletSettlementBills(): Promise<WalletSettlementBillsData> {
|
||||
return lotteryRequest.get<WalletSettlementBillsData>(
|
||||
`/wallet/settlement-bills`,
|
||||
);
|
||||
}
|
||||
|
||||
/** `POST /api/v1/wallet/transfer-in`(主站 → 彩票) */
|
||||
export function postWalletTransferIn(
|
||||
body: WalletTransferBody,
|
||||
@@ -58,3 +66,8 @@ export type {
|
||||
WalletLogsData,
|
||||
WalletPendingTransfer,
|
||||
} from "@/types/api/wallet-logs";
|
||||
export type {
|
||||
WalletSettlementBillItem,
|
||||
WalletSettlementBillsData,
|
||||
WalletSettlementBillsSummary,
|
||||
} from "@/types/api/wallet-settlement-bills";
|
||||
|
||||
@@ -48,11 +48,33 @@ const FLOW_LABEL_FALLBACKS: Record<string, string> = {
|
||||
"wallet.creditFlow.reversal": "退本释额",
|
||||
};
|
||||
|
||||
const CREDIT_BIZ_LABEL_FALLBACKS: Record<string, string> = {
|
||||
bet_hold: "下注占用",
|
||||
bet_hold_release: "退本释额",
|
||||
game_settlement_loss: "亏损入账",
|
||||
game_settlement_win: "中奖释额",
|
||||
game_settlement_reversal: "结算冲正",
|
||||
settlement_confirm: "账期收款",
|
||||
settlement_payout: "账期付款",
|
||||
rebate_accrued: "账期回水",
|
||||
rebate_in_bill: "回水已入账单",
|
||||
rebate_settled: "回水已结清",
|
||||
};
|
||||
|
||||
export function logTypeLabel(
|
||||
type: string,
|
||||
t?: (key: string, options?: { defaultValue?: string }) => string,
|
||||
creditMode = false,
|
||||
bizType?: string,
|
||||
): string {
|
||||
if (creditMode && bizType) {
|
||||
const bizKey = `wallet.creditBiz.${bizType}`;
|
||||
const fallback = CREDIT_BIZ_LABEL_FALLBACKS[bizType];
|
||||
if (fallback) {
|
||||
return t?.(bizKey, { defaultValue: fallback }) ?? fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = creditMode ? "wallet.creditFlow." : "wallet.flow.";
|
||||
const key = `${prefix}${type}`;
|
||||
return t?.(key, { defaultValue: FLOW_LABEL_FALLBACKS[key] ?? type }) ?? type;
|
||||
@@ -227,7 +249,7 @@ export function LogRow({
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={isIn ? "size-2.5 rounded-full bg-emerald-500" : "size-2.5 rounded-full bg-[#e5002c]"} aria-hidden />
|
||||
<p className="truncate text-base font-black leading-tight text-[#101a33]">
|
||||
{logTypeLabel(item.type, t, creditMode)}
|
||||
{logTypeLabel(item.type, t, creditMode, item.biz_type)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
||||
@@ -253,12 +275,18 @@ export function LogRow({
|
||||
|
||||
<div className="mt-3 flex items-center justify-between rounded-xl bg-[#f8fbff] px-3 py-2 text-xs">
|
||||
<span className="font-semibold text-slate-500">
|
||||
{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}
|
||||
{creditMode
|
||||
? item.affects_available_credit === false
|
||||
? t("wallet.creditBillOnly", { defaultValue: "账期记录" })
|
||||
: 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)
|
||||
: "—"}
|
||||
: creditMode && item.affects_available_credit === false
|
||||
? t("wallet.noCreditChange", { defaultValue: "不改变可用信用" })
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Wallet } from "lucide-react";
|
||||
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 } from "@/api/wallet";
|
||||
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";
|
||||
@@ -19,11 +19,16 @@ 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 { 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;
|
||||
@@ -49,6 +54,128 @@ function resolveFundingModeView(
|
||||
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): string {
|
||||
const start = item.period_start ? formatPlayerInstant(item.period_start).slice(0, 10) : "—";
|
||||
const end = item.period_end ? formatPlayerInstant(item.period_end).slice(0, 10) : "—";
|
||||
return `${start} / ${end}`;
|
||||
}
|
||||
|
||||
function CreditSettlementBillsBlock({
|
||||
data,
|
||||
currency,
|
||||
}: {
|
||||
data: WalletSettlementBillsData | null;
|
||||
currency: 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)}</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();
|
||||
@@ -58,6 +185,7 @@ export function WalletScreen() {
|
||||
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);
|
||||
@@ -141,6 +269,12 @@ export function WalletScreen() {
|
||||
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));
|
||||
@@ -220,13 +354,18 @@ export function WalletScreen() {
|
||||
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, t]);
|
||||
}, [currency, loadLogs, profile, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => void refreshAll();
|
||||
@@ -338,6 +477,10 @@ export function WalletScreen() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{fundingModeKnown && isCreditPlayer ? (
|
||||
<CreditSettlementBillsBlock data={settlementBills} currency={currency} />
|
||||
) : null}
|
||||
|
||||
{fundingModeKnown && !isCreditPlayer ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<TransferInDialog
|
||||
|
||||
28
src/types/api/wallet-settlement-bills.ts
Normal file
28
src/types/api/wallet-settlement-bills.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type WalletSettlementBillDirection = "receivable" | "payable";
|
||||
|
||||
export type WalletSettlementBillItem = {
|
||||
id: number;
|
||||
settlement_period_id: number;
|
||||
period_start: string | null;
|
||||
period_end: string | null;
|
||||
gross_win_loss: number;
|
||||
rebate_amount: number;
|
||||
net_amount: number;
|
||||
paid_amount: number;
|
||||
unpaid_amount: number;
|
||||
direction: WalletSettlementBillDirection;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type WalletSettlementBillsSummary = {
|
||||
pending_receivable: number;
|
||||
pending_payable: number;
|
||||
net_pending: number;
|
||||
pending_count: number;
|
||||
};
|
||||
|
||||
export type WalletSettlementBillsData = {
|
||||
items: WalletSettlementBillItem[];
|
||||
summary: WalletSettlementBillsSummary;
|
||||
funding_mode: string;
|
||||
};
|
||||
Reference in New Issue
Block a user