feat: enhance wallet functionality and improve error handling
- Updated .env.example to include optional player site code configuration. - Modified error messages in hall-bet-errors for better clarity. - Improved the betting grid logic to handle invalid draw IDs and reload draws when necessary. - Adjusted wallet components to differentiate between credit and main wallet balances, including new translations for credit-related terms. - Enhanced wallet logs to support credit activity tracking and improved UI for displaying credit information.
This commit is contained in:
@@ -20,11 +20,38 @@ export const WALLET_FLOW_FILTERS: { value: string; labelKey: string }[] = [
|
||||
{ value: "reversal", labelKey: "wallet.flow.reversal" },
|
||||
];
|
||||
|
||||
/** 信用盘:无主站划转;文案强调占用/释放信用 */
|
||||
export const CREDIT_FLOW_FILTERS: { value: string; labelKey: string }[] = [
|
||||
{ value: "", labelKey: "wallet.creditFlow.all" },
|
||||
{ value: "bet", labelKey: "wallet.creditFlow.bet" },
|
||||
{ value: "prize", labelKey: "wallet.creditFlow.prize" },
|
||||
{ value: "refund", labelKey: "wallet.creditFlow.refund" },
|
||||
{ value: "reversal", labelKey: "wallet.creditFlow.reversal" },
|
||||
];
|
||||
|
||||
const FLOW_LABEL_FALLBACKS: Record<string, string> = {
|
||||
"wallet.flow.all": "全部",
|
||||
"wallet.flow.transfer_in": "转入",
|
||||
"wallet.flow.transfer_out": "转出",
|
||||
"wallet.flow.bet": "下注",
|
||||
"wallet.flow.prize": "派奖",
|
||||
"wallet.flow.refund": "退款",
|
||||
"wallet.flow.reversal": "冲正",
|
||||
"wallet.creditFlow.all": "全部",
|
||||
"wallet.creditFlow.bet": "下注占用",
|
||||
"wallet.creditFlow.prize": "派奖释放",
|
||||
"wallet.creditFlow.refund": "退款返还",
|
||||
"wallet.creditFlow.reversal": "冲正回退",
|
||||
};
|
||||
|
||||
export function logTypeLabel(
|
||||
type: string,
|
||||
t?: (key: string, options?: { defaultValue?: string }) => string,
|
||||
creditMode = false,
|
||||
): string {
|
||||
return t?.(`wallet.flow.${type}`, { defaultValue: type }) ?? type;
|
||||
const prefix = creditMode ? "wallet.creditFlow." : "wallet.flow.";
|
||||
const key = `${prefix}${type}`;
|
||||
return t?.(key, { defaultValue: FLOW_LABEL_FALLBACKS[key] ?? type }) ?? type;
|
||||
}
|
||||
|
||||
function txnStatusLabel(
|
||||
@@ -46,6 +73,8 @@ type WalletLogsBlockProps = {
|
||||
currency: string;
|
||||
/** 独立流水页可隐藏标题或改文案 */
|
||||
title?: string;
|
||||
/** 信用盘:专用筛选项与列表文案 */
|
||||
creditMode?: boolean;
|
||||
};
|
||||
|
||||
/** 类型筛选 + 列表(待对账见顶栏通知铃铛) */
|
||||
@@ -60,23 +89,37 @@ export function WalletLogsBlock({
|
||||
onFilterChange,
|
||||
currency,
|
||||
title,
|
||||
creditMode = false,
|
||||
}: WalletLogsBlockProps) {
|
||||
const { t } = useTranslation("player");
|
||||
const resolvedTitle = title ?? t("wallet.flowsTitle");
|
||||
const filters = useMemo(
|
||||
() =>
|
||||
WALLET_FLOW_FILTERS.map((f) => ({
|
||||
...f,
|
||||
label: t(f.labelKey),
|
||||
})),
|
||||
[t],
|
||||
);
|
||||
const resolvedTitle =
|
||||
title
|
||||
?? (creditMode
|
||||
? t("wallet.creditFlowsTitle", { defaultValue: "信用流水" })
|
||||
: t("wallet.flowsTitle", { defaultValue: "钱包流水" }));
|
||||
const channelHint = creditMode
|
||||
? t("wallet.playerChannel.credit", {
|
||||
defaultValue: "信用盘玩家(占用与释放额度流水)",
|
||||
})
|
||||
: t("wallet.playerChannel.wallet", {
|
||||
defaultValue: "主站钱包玩家(划转与钱包余额流水)",
|
||||
});
|
||||
const filters = useMemo(() => {
|
||||
const source = creditMode ? CREDIT_FLOW_FILTERS : WALLET_FLOW_FILTERS;
|
||||
return source.map((f) => ({
|
||||
...f,
|
||||
label: t(f.labelKey, { defaultValue: FLOW_LABEL_FALLBACKS[f.labelKey] ?? f.labelKey }),
|
||||
}));
|
||||
}, [creditMode, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-black text-[#0b3f96]">{resolvedTitle}</h2>
|
||||
<div>
|
||||
<h2 className="text-sm font-black text-[#0b3f96]">{resolvedTitle}</h2>
|
||||
<p className="mt-0.5 text-[11px] font-medium text-slate-500">{channelHint}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{filters.map((f) => (
|
||||
<Button
|
||||
@@ -113,7 +156,12 @@ export function WalletLogsBlock({
|
||||
</li>
|
||||
) : (
|
||||
logs.items.map((row) => (
|
||||
<LogRow key={row.log_id} item={row} currency={currency} />
|
||||
<LogRow
|
||||
key={row.log_id}
|
||||
item={row}
|
||||
currency={currency}
|
||||
creditMode={creditMode}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
@@ -149,9 +197,11 @@ export function WalletLogsBlock({
|
||||
export function LogRow({
|
||||
item,
|
||||
currency,
|
||||
creditMode = false,
|
||||
}: {
|
||||
item: WalletLogItem;
|
||||
currency: string;
|
||||
creditMode?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation("player");
|
||||
const ccy = item.currency_code || currency;
|
||||
@@ -171,7 +221,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-[#0b3f96]"} aria-hidden />
|
||||
<p className="truncate text-base font-black leading-tight text-[#101a33]">
|
||||
{logTypeLabel(item.type, t)}
|
||||
{logTypeLabel(item.type, t, creditMode)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
||||
@@ -196,7 +246,9 @@ export function LogRow({
|
||||
</div>
|
||||
|
||||
<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">{t("wallet.balanceAfter")}</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]">
|
||||
{formatMinorAsCurrency(item.balance_after, ccy)}
|
||||
</span>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getWalletLogs } from "@/api/wallet";
|
||||
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";
|
||||
@@ -24,6 +24,7 @@ export function WalletLogsScreen() {
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [creditMode, setCreditMode] = useState(false);
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const fetchPassRef = useRef(true);
|
||||
@@ -39,6 +40,10 @@ export function WalletLogsScreen() {
|
||||
setLogsLoading(true);
|
||||
}
|
||||
try {
|
||||
const balance = await getWalletBalance({ currency });
|
||||
setCreditMode(
|
||||
balance.credit_line_mode === true || balance.funding_mode === "credit",
|
||||
);
|
||||
const nextLogs = await getWalletLogs({
|
||||
page: targetPage,
|
||||
size: WALLET_LOGS_PAGE_SIZE,
|
||||
@@ -129,6 +134,7 @@ export function WalletLogsScreen() {
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
currency={currency}
|
||||
creditMode={creditMode}
|
||||
title={t("wallet.typeFilter")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -112,6 +112,12 @@ export function WalletScreen() {
|
||||
|
||||
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
|
||||
|
||||
const isCreditPlayer =
|
||||
balance?.credit_line_mode === true || balance?.funding_mode === "credit";
|
||||
const displayMinor = isCreditPlayer
|
||||
? Number(balance?.available_balance ?? 0)
|
||||
: Number(balance?.balance ?? 0);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!logs || !hasMore || loadingMore) return;
|
||||
|
||||
@@ -172,50 +178,69 @@ export function WalletScreen() {
|
||||
<Wallet className="size-7" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-semibold text-white/90">{t("wallet.balance")}</p>
|
||||
<p className="text-sm font-semibold text-white/90">
|
||||
{isCreditPlayer
|
||||
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
|
||||
: t("wallet.balance")}
|
||||
</p>
|
||||
{loading ? (
|
||||
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
|
||||
) : (
|
||||
<p className="mt-1 text-2xl font-black leading-none tabular-nums tracking-normal">
|
||||
{formatMinorAsCurrency(balance?.balance ?? 0, currency)}
|
||||
{formatMinorAsCurrency(displayMinor, currency)}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-white/75">
|
||||
{t("wallet.available", {
|
||||
amount: formatMinorAsCurrency(balance?.available_balance ?? 0, currency),
|
||||
})}
|
||||
{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>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
{isCreditPlayer ? (
|
||||
<p className="rounded-xl border border-[#d6e4ff] bg-[#f5f9ff] px-3 py-3 text-sm text-[#0b3f96]/85">
|
||||
{t("wallet.creditNoTransferHint", {
|
||||
defaultValue: "信用盘账号由代理授信,无需主站转入转出;额度调整请联系代理。",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<WalletLogsBlock
|
||||
creditMode={isCreditPlayer}
|
||||
logs={logs}
|
||||
logsLoading={loading || logsLoading}
|
||||
loadingMore={loadingMore}
|
||||
|
||||
Reference in New Issue
Block a user