docs: 新增 AI 代理学习偏好与信用盘术语规范

feat: 优化登录与入口页面加载体验

- 为登录页面添加 Suspense 边界,统一 fallback 样式
- 重构入口守卫逻辑,在布局阶段处理未登录重定向,避免闪烁
- 登录页面支持会话过期提示,并自动清理 URL 参数

feat: 信用盘与钱包盘文案差异化展示

- 底部导航「钱包」标签在信用盘模式下显示为「信用」
- 大厅余额条在信用盘模式下使用「可
This commit is contained in:
2026-06-12 20:48:10 +08:00
parent 20d2befa55
commit 7c283627d3
24 changed files with 347 additions and 92 deletions

View File

@@ -15,6 +15,7 @@ import {
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { useApiQuery } from "@/hooks/use-api-query";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { cn } from "@/lib/utils";
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
@@ -60,15 +61,21 @@ export function HallWalletStrip() {
}, [isDegraded]);
const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0);
const isCreditPlayer =
balance?.credit_line_mode === true || balance?.funding_mode === "credit";
const isCreditPlayer = isCreditFundingPlayer(balance);
const mainMinor =
balance?.main_balance === null || balance?.main_balance === undefined
? null
: Number(balance.main_balance);
return (
<section className="mb-3 space-y-2.5" aria-label={t("wallet.balance")}>
<section
className="mb-3 space-y-2.5"
aria-label={
isCreditPlayer
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
: t("wallet.balance")
}
>
<div
className={cn(
"relative overflow-hidden rounded-xl bg-[#e5002c] px-3 py-3 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
@@ -87,7 +94,7 @@ export function HallWalletStrip() {
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-white/90">
{balance?.credit_line_mode
{isCreditPlayer
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
: t("wallet.balance")}
</p>

View File

@@ -5,6 +5,7 @@ export function ticketStatusDisplay(
winMinor: number,
jackpotMinor: number,
t?: (key: string, options?: { defaultValue?: string; status?: string }) => string,
creditMode = false,
): { label: string; dotClass: string; ring?: boolean } {
const total = winMinor + jackpotMinor;
if (status === "partial_failed") {
@@ -32,10 +33,20 @@ export function ticketStatusDisplay(
};
}
if (status === "pending_payout") {
return { label: t?.("ticketStatus.pending_payout") ?? status, dotClass: "bg-amber-500" };
return {
label: creditMode
? t?.("ticketStatus.credit.pending_payout", { defaultValue: "已中奖待结算" }) ?? status
: t?.("ticketStatus.pending_payout") ?? status,
dotClass: "bg-amber-500",
};
}
if (status === "settled_win" && total > 0) {
return { label: t?.("ticketStatus.settled_win") ?? status, dotClass: "bg-emerald-500" };
return {
label: creditMode
? t?.("ticketStatus.credit.settled_win", { defaultValue: "已中奖" }) ?? status
: t?.("ticketStatus.settled_win") ?? status,
dotClass: "bg-emerald-500",
};
}
if (status === "settled_lose" || (status === "settled_win" && total <= 0)) {
return {

View File

@@ -22,8 +22,10 @@ import { orderGroupPath } from "@/features/orders/group-ticket-items";
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
import { formatPlayerInstant } from "@/lib/player-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { norm4d } from "@/lib/norm-4d";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import { playLabel } from "@/lib/play-labels";
import { cn } from "@/lib/utils";
import type { TicketItemDetailPayload } from "@/types/api/ticket-items";
@@ -79,6 +81,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
const searchParams = useSearchParams();
const { t } = useTranslation("player");
const { activeCurrency } = useActivePlayerCurrency();
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
useCurrencyCatalog();
const [data, setData] = useState<TicketItemDetailPayload | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -184,7 +187,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
}
const cur = data.currency_code ?? activeCurrency;
const st = ticketStatusDisplay(data.status, data.win_amount, data.jackpot_win_amount, t);
const st = ticketStatusDisplay(data.status, data.win_amount, data.jackpot_win_amount, t, creditMode);
const orderStatus = data.order_status ?? null;
const isPartialFailedOrder = orderStatus === "partial_failed";
const isLineFailed = data.status === "failed" || data.status === "refunded";
@@ -370,7 +373,9 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
) : null}
</p>
<p className="mt-1 font-mono font-semibold text-emerald-900">
{t("orders.payoutTotal", { amount: formatMinorAsCurrency(totalWin, cur) })}
{t(creditMode ? "orders.creditWinTotal" : "orders.payoutTotal", {
amount: formatMinorAsCurrency(totalWin, cur),
})}
</p>
</div>
) : hasSettlement ? (

View File

@@ -16,8 +16,10 @@ import {
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { playLabel } from "@/lib/play-labels";
import { cn } from "@/lib/utils";
import { usePlayerSessionStore } from "@/stores/player-session-store";
type TicketOrderGroupScreenProps = {
groupKey: string;
@@ -26,6 +28,7 @@ type TicketOrderGroupScreenProps = {
export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps) {
const { t } = useTranslation("player");
const { activeCurrency } = useActivePlayerCurrency();
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
useCurrencyCatalog();
const decodedKey = decodeURIComponent(groupKey);
@@ -61,6 +64,7 @@ export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps
group.win_amount,
group.jackpot_win_amount,
t,
creditMode,
);
const totalWin = group.win_amount + group.jackpot_win_amount;
return (
@@ -116,6 +120,7 @@ export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps
row.win_amount,
row.jackpot_win_amount,
t,
creditMode,
);
const lineWin = row.win_amount + row.jackpot_win_amount;
return (

View File

@@ -26,6 +26,7 @@ import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
import { useIsMobile } from "@/hooks/use-mobile";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import {
formatSchedulePickerYmd,
getTimeZoneShortLabel,
@@ -33,6 +34,7 @@ import {
scheduleTodayYmd,
} from "@/lib/player-datetime";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import { playLabel } from "@/lib/play-labels";
import { cn } from "@/lib/utils";
import type { TicketItemListRow } from "@/types/api/ticket-items";
@@ -55,6 +57,7 @@ export function TicketOrdersListScreen() {
const searchParams = useSearchParams();
const { t } = useTranslation("player");
const { activeCurrency } = useActivePlayerCurrency();
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
useCurrencyCatalog();
const drawNoFilter = useMemo(() => (searchParams.get("draw_no") ?? "").trim(), [searchParams]);
const statusFilter = useMemo(
@@ -427,6 +430,7 @@ export function TicketOrdersListScreen() {
group.win_amount,
group.jackpot_win_amount,
t,
creditMode,
);
const totalWin = group.win_amount + group.jackpot_win_amount;
return (

View File

@@ -12,7 +12,7 @@ import {
} from "lucide-react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { getPublicCurrencies } from "@/api/currency";
@@ -116,6 +116,27 @@ export function EntryGate() {
!tokenFromUrl &&
!(bearerToken ?? "").trim();
const [gateReady, setGateReady] = useState(false);
useLayoutEffect(() => {
if (typeof window === "undefined") return;
if (!isInIframe()) {
if (sessionExpired) {
router.replace("/login?session=expired");
return;
}
const hasToken = tokenFromUrl !== "" || (bearerToken ?? "").trim() !== "";
if (!hasToken) {
router.replace("/login");
return;
}
}
setGateReady(true);
}, [bearerToken, router, sessionExpired, tokenFromUrl]);
const [phase, setPhase] = useState<Phase>(sessionExpired ? "failed" : "loading");
const [failureDetails, setFailureDetails] = useState<FailureRow[]>(() =>
sessionExpired
@@ -300,6 +321,10 @@ export function EntryGate() {
return () => window.clearTimeout(tmr);
}, [sessionExpired, effectiveToken, tokenFromUrl]);
if (!gateReady) {
return null;
}
return (
<div className="relative flex min-h-dvh flex-col bg-white">
<div className={cn("relative h-[45vh] min-h-[320px]", phase === "success" ? "bg-white" : "bg-red-600")}>

View File

@@ -2,9 +2,8 @@
import { Loader2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -20,20 +19,42 @@ import { LotteryApiBizError } from "@/types/api/errors";
const DEPLOY_SITE_CODE = process.env.NEXT_PUBLIC_PLAYER_SITE_CODE?.trim() ?? "";
function stripSearchParamFromBrowserUrl(name: string): void {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
if (!url.searchParams.has(name)) return;
url.searchParams.delete(name);
const next = `${url.pathname}${url.search}${url.hash}`;
window.history.replaceState(null, "", next);
}
export function PlayerLoginScreen(): React.ReactElement {
const { t } = useTranslation("entry");
const router = useRouter();
const searchParams = useSearchParams();
const setBearerToken = usePlayerSessionStore((s) => s.setBearerToken);
const setProfile = usePlayerSessionStore((s) => s.setProfile);
const clearBearerToken = usePlayerSessionStore((s) => s.clearBearerToken);
const sessionExpiredHandled = useRef(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
if (sessionExpiredHandled.current) return;
if (searchParams.get("session") !== "expired") return;
sessionExpiredHandled.current = true;
clearBearerToken();
toast.error(t("errors.sessionExpired"));
stripSearchParamFromBrowserUrl("session");
}, [clearBearerToken, searchParams, t]);
async function handleSubmit(event: React.FormEvent): Promise<void> {
event.preventDefault();
if (!username.trim() || !password) {
toast.error(t("login.missingFields", { defaultValue: "请填写账号和密码" }));
toast.error(t("login.missingFields"));
return;
}
@@ -55,7 +76,7 @@ export function PlayerLoginScreen(): React.ReactElement {
}
router.replace("/hall");
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("login.failed", { defaultValue: "登录失败" }));
toast.error(e instanceof LotteryApiBizError ? e.message : t("login.failed"));
} finally {
setLoading(false);
}
@@ -63,8 +84,10 @@ export function PlayerLoginScreen(): React.ReactElement {
return (
<div className="relative flex min-h-dvh flex-col bg-white">
<div className="relative h-[32vh] min-h-[220px] bg-red-600">
<Image src="/entry/image1.png" alt="" fill className="object-cover object-center" priority />
<div className="relative h-[45vh] min-h-[320px] shrink-0 overflow-hidden bg-red-600">
<div className="pointer-events-none absolute inset-0">
<Image src="/entry/image1.png" alt="" fill className="object-cover object-center" priority />
</div>
<div className="absolute left-0 right-0 top-0 z-20 flex items-center px-4 py-3">
<LanguageSwitcher variant="header" showFlag={false} />
</div>
@@ -72,15 +95,15 @@ export function PlayerLoginScreen(): React.ReactElement {
<div className="mx-auto w-full max-w-md flex-1 px-4 py-8">
<h1 className="text-xl font-bold text-gray-900">
{t("login.title", { defaultValue: "代理玩家登录" })}
{t("login.title")}
</h1>
<p className="mt-2 text-sm text-muted-foreground">
{t("login.hint", { defaultValue: "使用代理为您开通的账号登录。主站用户请从主站进入。" })}
{t("login.hint")}
</p>
<form className="mt-6 space-y-4" onSubmit={(e) => void handleSubmit(e)}>
<div className="space-y-1">
<Label htmlFor="login-user">{t("login.username", { defaultValue: "登录账号" })}</Label>
<Label htmlFor="login-user">{t("login.username")}</Label>
<Input
id="login-user"
value={username}
@@ -89,7 +112,7 @@ export function PlayerLoginScreen(): React.ReactElement {
/>
</div>
<div className="space-y-1">
<Label htmlFor="login-pass">{t("login.password", { defaultValue: "密码" })}</Label>
<Label htmlFor="login-pass">{t("login.password")}</Label>
<Input
id="login-pass"
type="password"
@@ -100,15 +123,9 @@ export function PlayerLoginScreen(): React.ReactElement {
</div>
<Button type="submit" className="w-full bg-red-600 hover:bg-red-700" disabled={loading}>
{loading ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
{t("login.submit", { defaultValue: "登录" })}
{t("login.submit")}
</Button>
</form>
<p className="mt-4 text-center text-sm text-muted-foreground">
<Link href="/" className="text-red-600 underline">
{t("login.backEntry", { defaultValue: "返回入口" })}
</Link>
</p>
</div>
</div>
);

View File

@@ -22,9 +22,11 @@ import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
import { getPlayerBearerTokenPayload } from "@/lib/lottery-auth";
import { formatPlayerInstant } from "@/lib/player-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { norm4d } from "@/lib/norm-4d";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { cn } from "@/lib/utils";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import type { DrawResultDetailPayload } from "@/types/api/draw-results";
type DrawResultDetailScreenProps = {
@@ -35,6 +37,7 @@ type DrawResultDetailScreenProps = {
export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) {
const { t } = useTranslation("player");
const { activeCurrency } = useActivePlayerCurrency();
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
useCurrencyCatalog();
const [data, setData] = useState<DrawResultDetailPayload | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -241,7 +244,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
{showMyPayout && myTotals ? (
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-3 text-sm shadow-[0_4px_14px_rgba(15,23,42,0.03)]">
<p className="font-bold text-emerald-900">
{t("results.myPayout")}
{t(creditMode ? "results.creditMyWin" : "results.myPayout")}
</p>
<p className="mt-2 font-mono text-xs tabular-nums text-emerald-900/80">
{t("results.regular", {
@@ -262,7 +265,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
{showHitOnly ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-3 text-xs text-amber-950">
{t("results.hitPending")}
{t(creditMode ? "results.creditHitPending" : "results.hitPending")}
</div>
) : null}

View File

@@ -24,9 +24,8 @@ export const WALLET_FLOW_FILTERS: { value: string; labelKey: string }[] = [
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" },
{ value: "credit_release", labelKey: "wallet.creditFlow.credit_release" },
{ value: "bill_settlement", labelKey: "wallet.creditFlow.bill_settlement" },
];
const FLOW_LABEL_FALLBACKS: Record<string, string> = {
@@ -38,10 +37,12 @@ const FLOW_LABEL_FALLBACKS: Record<string, string> = {
"wallet.flow.refund": "退款",
"wallet.flow.reversal": "冲正",
"wallet.creditFlow.all": "全部",
"wallet.creditFlow.bet": "下注占用",
"wallet.creditFlow.prize": "派奖释放",
"wallet.creditFlow.refund": "退款返还",
"wallet.creditFlow.reversal": "冲正回退",
"wallet.creditFlow.bet": "占用额度",
"wallet.creditFlow.credit_release": "释额",
"wallet.creditFlow.bill_settlement": "账期收付",
"wallet.creditFlow.win_credit": "中奖释额",
"wallet.creditFlow.refund": "账期确认释额",
"wallet.creditFlow.reversal": "退本释额",
};
export function logTypeLabel(
@@ -97,13 +98,6 @@ export function WalletLogsBlock({
?? (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) => ({
@@ -118,7 +112,6 @@ export function WalletLogsBlock({
<div className="flex flex-col gap-2">
<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) => (
@@ -152,7 +145,9 @@ export function WalletLogsBlock({
<ul className="space-y-2">
{logs.items.length === 0 ? (
<li className="rounded-lg border border-dashed py-8 text-center text-sm text-muted-foreground">
{t("wallet.emptyLogs")}
{t(creditMode ? "wallet.emptyCreditLogs" : "wallet.emptyLogs", {
defaultValue: creditMode ? "暂无信用流水" : "暂无流水",
})}
</li>
) : (
logs.items.map((row) => (
@@ -219,7 +214,7 @@ export function LogRow({
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<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 />
<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)}
</p>
@@ -235,7 +230,7 @@ export function LogRow({
</div>
<div className="flex shrink-0 flex-col items-end gap-2 text-right">
<p className={isIn ? "text-lg font-black tabular-nums text-emerald-600" : "text-lg font-black tabular-nums text-[#0b3f96]"}>
<p className={isIn ? "text-lg font-black tabular-nums text-emerald-600" : "text-lg font-black tabular-nums text-[#e5002c]"}>
{isIn ? "+" : ""}
{formatMinorAsCurrency(item.amount_abs, ccy)}
</p>
@@ -246,12 +241,24 @@ 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">
{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}
</span>
<span className="font-mono font-black tabular-nums text-[#32518d]">
{formatMinorAsCurrency(item.balance_after, ccy)}
</span>
{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>
</>
)}
</div>
</li>
);

View File

@@ -10,6 +10,7 @@ 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 { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
@@ -106,9 +107,17 @@ export function WalletLogsScreen() {
return (
<PlayerPanel
title={t("wallet.logsTitle")}
title={
creditMode
? t("wallet.creditLogsTitle", { defaultValue: "信用流水" })
: t("wallet.logsTitle")
}
backHref="/wallet"
backLabel={t("wallet.title")}
backLabel={
creditMode
? t("wallet.creditTitle", { defaultValue: "信用" })
: t("wallet.title")
}
>
<div className="space-y-3">
{error ? (

View File

@@ -16,6 +16,7 @@ import {
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 { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { formatMinorAsCurrency } from "@/lib/money";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { formatWalletClientError } from "@/lib/wallet-api-error";
@@ -117,8 +118,7 @@ export function WalletScreen() {
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
const isCreditPlayer =
balance?.credit_line_mode === true || balance?.funding_mode === "credit";
const isCreditPlayer = isCreditFundingPlayer(balance);
const displayMinor = isCreditPlayer
? Number(balance?.available_balance ?? 0)
: Number(balance?.balance ?? 0);
@@ -155,7 +155,9 @@ export function WalletScreen() {
}, [hasMore, loadMore, loading, loadingMore, logsLoading]);
return (
<PlayerPanel title={t("wallet.title")}>
<PlayerPanel
title={isCreditPlayer ? 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">
@@ -213,7 +215,8 @@ export function WalletScreen() {
{isCreditPlayer ? (
<p className="rounded-xl border border-[#d6e4ff] bg-[#f5f9ff] px-3 py-3 text-sm text-[#0b3f96]/85">
{t("wallet.creditNoTransferHint", {
defaultValue: "信用盘账号由代理授信,无需主站转入转出;额度调整请联系代理。",
defaultValue:
"由代理授信,无需主站转入转出;中奖不即时派彩,盈亏在账期统一结算,额度调整请联系代理。",
})}
</p>
) : (