- 新增接口 getWalletSettlementBills 获取信用盘玩家账期账单数据 - 在钱包日志块中添加业务类型标签支持,增强日志类型显示 - WalletScreen 中增加账期账单数据的加载和展示逻辑 - 新增 CreditSettlementBillsBlock 组件用于展示账期账单摘要和详情列表 - 支持账单状态显示和账期区间格式化 - 仅信用盘玩家显示账期账单模块,其他类型玩家隐藏 - 新增 wallet-settlement-bills.ts 类型定义文件,完善账期账单数据结构
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
|||||||
WalletTransferResultData,
|
WalletTransferResultData,
|
||||||
} from "@/types/api/wallet-transfer";
|
} from "@/types/api/wallet-transfer";
|
||||||
import type { GetWalletLogsParams, WalletLogsData } from "@/types/api/wallet-logs";
|
import type { GetWalletLogsParams, WalletLogsData } from "@/types/api/wallet-logs";
|
||||||
|
import type { WalletSettlementBillsData } from "@/types/api/wallet-settlement-bills";
|
||||||
|
|
||||||
export type GetWalletBalanceParams = {
|
export type GetWalletBalanceParams = {
|
||||||
/** Query `currency`,不传则用玩家默认币种 */
|
/** 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`(主站 → 彩票) */
|
/** `POST /api/v1/wallet/transfer-in`(主站 → 彩票) */
|
||||||
export function postWalletTransferIn(
|
export function postWalletTransferIn(
|
||||||
body: WalletTransferBody,
|
body: WalletTransferBody,
|
||||||
@@ -58,3 +66,8 @@ export type {
|
|||||||
WalletLogsData,
|
WalletLogsData,
|
||||||
WalletPendingTransfer,
|
WalletPendingTransfer,
|
||||||
} from "@/types/api/wallet-logs";
|
} 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": "退本释额",
|
"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(
|
export function logTypeLabel(
|
||||||
type: string,
|
type: string,
|
||||||
t?: (key: string, options?: { defaultValue?: string }) => string,
|
t?: (key: string, options?: { defaultValue?: string }) => string,
|
||||||
creditMode = false,
|
creditMode = false,
|
||||||
|
bizType?: string,
|
||||||
): 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 prefix = creditMode ? "wallet.creditFlow." : "wallet.flow.";
|
||||||
const key = `${prefix}${type}`;
|
const key = `${prefix}${type}`;
|
||||||
return t?.(key, { defaultValue: FLOW_LABEL_FALLBACKS[key] ?? type }) ?? 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">
|
<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 />
|
<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]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
<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">
|
<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">
|
<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>
|
||||||
<span className="font-mono font-black tabular-nums text-[#32518d]">
|
<span className="font-mono font-black tabular-nums text-[#32518d]">
|
||||||
{item.balance_after != null
|
{item.balance_after != null
|
||||||
? formatMinorAsCurrency(item.balance_after, ccy)
|
? formatMinorAsCurrency(item.balance_after, ccy)
|
||||||
: "—"}
|
: creditMode && item.affects_available_credit === false
|
||||||
|
? t("wallet.noCreditChange", { defaultValue: "不改变可用信用" })
|
||||||
|
: "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Wallet } from "lucide-react";
|
import { ArrowDownLeft, ArrowUpRight, Wallet } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
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 { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
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";
|
||||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||||
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||||
import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
|
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";
|
import type { PlayerMeData } from "@/types/api/player-me";
|
||||||
|
|
||||||
const WALLET_LOGS_PAGE_SIZE = 10;
|
const WALLET_LOGS_PAGE_SIZE = 10;
|
||||||
@@ -49,6 +54,128 @@ function resolveFundingModeView(
|
|||||||
return "unknown";
|
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() {
|
export function WalletScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -58,6 +185,7 @@ export function WalletScreen() {
|
|||||||
const [transferOutOpen, setTransferOutOpen] = useState(false);
|
const [transferOutOpen, setTransferOutOpen] = useState(false);
|
||||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||||
const [logs, setLogs] = useState<WalletLogsData | null>(null);
|
const [logs, setLogs] = useState<WalletLogsData | null>(null);
|
||||||
|
const [settlementBills, setSettlementBills] = useState<WalletSettlementBillsData | null>(null);
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [logsLoading, setLogsLoading] = useState(false);
|
const [logsLoading, setLogsLoading] = useState(false);
|
||||||
@@ -141,6 +269,12 @@ export function WalletScreen() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setBalance(b);
|
setBalance(b);
|
||||||
setLogs(nextLogs);
|
setLogs(nextLogs);
|
||||||
|
if (isCreditFundingPlayer(b) || isCreditFundingPlayer(profile)) {
|
||||||
|
const bills = await getWalletSettlementBills();
|
||||||
|
if (!cancelled) setSettlementBills(bills);
|
||||||
|
} else {
|
||||||
|
setSettlementBills(null);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError(formatWalletClientError(e, t));
|
setError(formatWalletClientError(e, t));
|
||||||
@@ -220,13 +354,18 @@ export function WalletScreen() {
|
|||||||
loadLogs(1, false),
|
loadLogs(1, false),
|
||||||
]);
|
]);
|
||||||
setBalance(b);
|
setBalance(b);
|
||||||
|
if (isCreditFundingPlayer(b) || isCreditFundingPlayer(profile)) {
|
||||||
|
setSettlementBills(await getWalletSettlementBills());
|
||||||
|
} else {
|
||||||
|
setSettlementBills(null);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatWalletClientError(e, t));
|
setError(formatWalletClientError(e, t));
|
||||||
} finally {
|
} finally {
|
||||||
setLogsLoading(false);
|
setLogsLoading(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currency, loadLogs, t]);
|
}, [currency, loadLogs, profile, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onCurrencyChange = () => void refreshAll();
|
const onCurrencyChange = () => void refreshAll();
|
||||||
@@ -338,6 +477,10 @@ export function WalletScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{fundingModeKnown && isCreditPlayer ? (
|
||||||
|
<CreditSettlementBillsBlock data={settlementBills} currency={currency} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{fundingModeKnown && !isCreditPlayer ? (
|
{fundingModeKnown && !isCreditPlayer ? (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<TransferInDialog
|
<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