refactor(hall): 优化投注格子行数与号码输入逻辑
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字段表示账期时区
This commit is contained in:
2026-07-09 15:48:36 +08:00
parent 4a6f021304
commit 56ba12fbfc
8 changed files with 173 additions and 43 deletions

View File

@@ -150,6 +150,11 @@ const D4_PLAY_ORDER = [
"digit_small", "digit_small",
] as const; ] as const;
const MOBILE_QUICK_AMOUNT_PRESETS = ["10", "50", "100"] as const; const MOBILE_QUICK_AMOUNT_PRESETS = ["10", "50", "100"] as const;
const DEFAULT_DRAFT_ROW_COUNT = 20;
function newDraftRows(count = DEFAULT_DRAFT_ROW_COUNT): DraftRow[] {
return Array.from({ length: count }, newDraftRow);
}
function newDraftRow(): DraftRow { function newDraftRow(): DraftRow {
const id = const id =
@@ -215,8 +220,18 @@ function playColumnsForCategory(
}); });
} }
function sanitizeNumber(raw: string): string { function numberMaxCharsForCategory(category: PlayHallCategory): number {
return raw.replace(/[^0-9Rr]/g, "").toUpperCase().slice(0, 4); return category === "D2" ? 2 : category === "D3" ? 3 : 4;
}
function sanitizeNumber(raw: string, category: PlayHallCategory): string {
const normalized =
category === "D4"
? raw.replace(/[^0-9Rr]/g, "").toUpperCase()
: raw.replace(/\D/g, "");
const maxChars = numberMaxCharsForCategory(category);
return category === "D4" ? normalized.slice(0, maxChars) : normalized.slice(-maxChars);
} }
function sanitizeAmount(raw: string): string { function sanitizeAmount(raw: string): string {
@@ -439,7 +454,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile)); const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [rows, setRows] = useState<DraftRow[]>(() => Array.from({ length: 20 }, newDraftRow)); const [rows, setRows] = useState<DraftRow[]>(() => newDraftRows());
const [activeRowId, setActiveRowId] = useState<string | null>(null); const [activeRowId, setActiveRowId] = useState<string | null>(null);
const [catalogState, setCatalogState] = useState< const [catalogState, setCatalogState] = useState<
| { kind: "loading" } | { kind: "loading" }
@@ -642,15 +657,16 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status)); const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status));
const numberPlaceholder = const numberPlaceholder =
activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000"; activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000";
const numberMaxChars = numberMaxCharsForCategory(activeCategory);
const updateRowNumber = useCallback((id: string, value: string) => { const updateRowNumber = useCallback((id: string, value: string) => {
setRows((current) => setRows((current) =>
current.map((row) => current.map((row) =>
row.id === id ? { ...row, number: sanitizeNumber(value) } : row, row.id === id ? { ...row, number: sanitizeNumber(value, activeCategory) } : row,
), ),
); );
setActiveRowId(id); setActiveRowId(id);
}, []); }, [activeCategory]);
const updateAmount = useCallback((rowId: string, playCode: string, value: string) => { const updateAmount = useCallback((rowId: string, playCode: string, value: string) => {
setRows((current) => setRows((current) =>
@@ -1151,7 +1167,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
setPreviewData(null); setPreviewData(null);
setResultData(data); setResultData(data);
setResultOpen(true); setResultOpen(true);
setRows([newDraftRow()]); setRows(newDraftRows());
setActiveRowId(null); setActiveRowId(null);
triggerWalletPollingAfterBet(); triggerWalletPollingAfterBet();
void refreshWallet(); void refreshWallet();
@@ -1752,6 +1768,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
rowActive={activeRowId === row.id} rowActive={activeRowId === row.id}
tableDisabled={tableDisabled} tableDisabled={tableDisabled}
numberPlaceholder={numberPlaceholder} numberPlaceholder={numberPlaceholder}
activeCategory={activeCategory}
numberMaxChars={numberMaxChars}
playColumns={playColumns} playColumns={playColumns}
alertRows={alertRows} alertRows={alertRows}
liveSoldOutNumbers={liveSoldOutNumbers} liveSoldOutNumbers={liveSoldOutNumbers}
@@ -1905,6 +1923,8 @@ const DraftRowItem = memo(function DraftRowItem({
rowActive, rowActive,
tableDisabled, tableDisabled,
numberPlaceholder, numberPlaceholder,
activeCategory,
numberMaxChars,
playColumns, playColumns,
alertRows, alertRows,
liveSoldOutNumbers, liveSoldOutNumbers,
@@ -1919,16 +1939,18 @@ const DraftRowItem = memo(function DraftRowItem({
indexColClass, indexColClass,
numberColClass, numberColClass,
stickyNumberLeftClass, stickyNumberLeftClass,
}: { }: {
row: DraftRow; row: DraftRow;
index: number; index: number;
rowActive: boolean; rowActive: boolean;
tableDisabled: boolean; tableDisabled: boolean;
numberPlaceholder: string; numberPlaceholder: string;
playColumns: PlayColumn[]; activeCategory: PlayHallCategory;
alertRows: DrawCurrentRiskPoolAlert[]; numberMaxChars: number;
liveSoldOutNumbers: Set<string>; playColumns: PlayColumn[];
liveWarningNumbers: Set<string>; alertRows: DrawCurrentRiskPoolAlert[];
liveSoldOutNumbers: Set<string>;
liveWarningNumbers: Set<string>;
updateRowNumber: (id: string, value: string) => void; updateRowNumber: (id: string, value: string) => void;
updateAmount: (rowId: string, playCode: string, value: string) => void; updateAmount: (rowId: string, playCode: string, value: string) => void;
setActiveRowId: (id: string) => void; setActiveRowId: (id: string) => void;
@@ -1937,10 +1959,12 @@ const DraftRowItem = memo(function DraftRowItem({
isMobile: boolean; isMobile: boolean;
removable: boolean; removable: boolean;
indexColClass: string; indexColClass: string;
numberColClass: string; numberColClass: string;
stickyNumberLeftClass: string; stickyNumberLeftClass: string;
}) { }) {
return ( const displayNumber = sanitizeNumber(row.number, activeCategory);
return (
<tr <tr
className={cn( className={cn(
"border-b border-[#f0f3f8] last:border-b-0", "border-b border-[#f0f3f8] last:border-b-0",
@@ -1964,13 +1988,14 @@ const DraftRowItem = memo(function DraftRowItem({
rowActive ? "bg-[#f5f9ff]" : "bg-white", rowActive ? "bg-[#f5f9ff]" : "bg-white",
)} )}
> >
<Input <Input
value={row.number} value={displayNumber}
disabled={tableDisabled} disabled={tableDisabled}
inputMode="numeric" inputMode="numeric"
pattern="[0-9Rr]*" pattern="[0-9Rr]*"
autoComplete="off" autoComplete="off"
placeholder={numberPlaceholder} placeholder={numberPlaceholder}
maxLength={numberMaxChars}
onFocus={() => setActiveRowId(row.id)} onFocus={() => setActiveRowId(row.id)}
onClick={() => setActiveRowId(row.id)} onClick={() => setActiveRowId(row.id)}
onChange={(event) => updateRowNumber(row.id, event.target.value)} onChange={(event) => updateRowNumber(row.id, event.target.value)}

View File

@@ -47,6 +47,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
const [data, setData] = useState<DrawResultDetailPayload | null>(null); const [data, setData] = useState<DrawResultDetailPayload | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeProviderCode, setActiveProviderCode] = useState<string>("");
const [highlightSet, setHighlightSet] = useState<ReadonlySet<string> | null>(null); const [highlightSet, setHighlightSet] = useState<ReadonlySet<string> | null>(null);
const [myTotals, setMyTotals] = useState<{ const [myTotals, setMyTotals] = useState<{
win: number; win: number;
@@ -60,6 +61,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
try { try {
const row = await getDrawResultByNo(drawNo, { currency: activeCurrency }); const row = await getDrawResultByNo(drawNo, { currency: activeCurrency });
setData(row); setData(row);
setActiveProviderCode(String(row.provider_code ?? row.provider_results?.[0]?.provider_code ?? ""));
} catch { } catch {
setData(null); setData(null);
setError(t("results.unavailable")); setError(t("results.unavailable"));
@@ -144,6 +146,12 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
} }
const currency = data.jackpot?.currency_code ?? activeCurrency; const currency = data.jackpot?.currency_code ?? activeCurrency;
const providerResults = data.provider_results ?? [];
const activeProviderResult =
providerResults.find((row) => String(row.provider_code ?? "") === activeProviderCode)
?? providerResults[0]
?? null;
const activeResults = activeProviderResult?.results ?? data.results;
const showMyPayout = const showMyPayout =
myTotals && myTotals &&
myTotals.hasBets && myTotals.hasBets &&
@@ -222,6 +230,29 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
{t("results.detail")} {t("results.detail")}
</span> </span>
</div> </div>
{providerResults.length > 1 ? (
<div className="mt-3 flex flex-wrap gap-2">
{providerResults.map((row) => {
const code = String(row.provider_code ?? "");
const active = code === String(activeProviderResult?.provider_code ?? "");
return (
<button
key={code || row.provider_name || row.result_version}
type="button"
onClick={() => setActiveProviderCode(code)}
className={cn(
"rounded-full border px-3 py-1 text-xs font-bold transition-colors",
active
? "border-[#0b56b7] bg-[#0b56b7] text-white"
: "border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",
)}
>
{row.provider_name || row.provider_code || "Provider"}
</button>
);
})}
</div>
) : null}
<div className="mt-3 grid grid-cols-3 gap-2 text-center"> <div className="mt-3 grid grid-cols-3 gap-2 text-center">
{RESULTS_TOP_PRIZE_KEYS.map((tier) => ( {RESULTS_TOP_PRIZE_KEYS.map((tier) => (
<div <div
@@ -232,7 +263,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
{t(resultsPrizeLabelKey(tier))} {t(resultsPrizeLabelKey(tier))}
</p> </p>
<p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]"> <p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]">
{data.results[tier]} {activeResults[tier]}
</p> </p>
</div> </div>
))} ))}
@@ -240,7 +271,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
</div> </div>
<TwentyThreeResultsGrid <TwentyThreeResultsGrid
numbers={data.results} numbers={activeResults}
highlighted4d={highlightSet ?? undefined} highlighted4d={highlightSet ?? undefined}
/> />

View File

@@ -252,6 +252,11 @@ export function DrawResultsListScreen() {
), ),
})} })}
</p> </p>
{(featured.provider_results?.length ?? 0) > 1 ? (
<p className="mt-1 text-[11px] font-semibold text-[#0b56b7]">
{featured.provider_results?.map((row) => row.provider_name || row.provider_code).filter(Boolean).join(" / ")}
</p>
) : null}
</div> </div>
<Link <Link
href={`/results/${encodeURIComponent(featured.draw_no)}`} href={`/results/${encodeURIComponent(featured.draw_no)}`}
@@ -291,6 +296,11 @@ export function DrawResultsListScreen() {
<p className="mt-1 text-[11px] text-slate-500"> <p className="mt-1 text-[11px] text-slate-500">
{formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)} {formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)}
</p> </p>
{(row.provider_results?.length ?? 0) > 1 ? (
<p className="mt-1 text-[11px] font-semibold text-[#0b56b7]">
{row.provider_results?.map((item) => item.provider_name || item.provider_code).filter(Boolean).join(" / ")}
</p>
) : null}
</div> </div>
<span className="shrink-0 rounded-full bg-[#f2f6ff] px-2.5 py-1 text-xs font-bold text-[#0b56b7]"> <span className="shrink-0 rounded-full bg-[#f2f6ff] px-2.5 py-1 text-xs font-bold text-[#0b56b7]">
{t("results.detail")} {t("results.detail")}

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
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 { formatPlayerInstant } from "@/lib/player-datetime"; import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime";
import { formatMinorAsCurrency } from "@/lib/money"; import { formatMinorAsCurrency } from "@/lib/money";
import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs"; import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs";
@@ -54,7 +54,7 @@ const CREDIT_BIZ_LABEL_FALLBACKS: Record<string, string> = {
game_settlement_loss: "亏损入账", game_settlement_loss: "亏损入账",
game_settlement_win: "中奖释额", game_settlement_win: "中奖释额",
game_settlement_reversal: "结算冲正", game_settlement_reversal: "结算冲正",
settlement_confirm: "账期收款", settlement_confirm: "账期还款释额",
settlement_payout: "账期付款", settlement_payout: "账期付款",
rebate_accrued: "账期回水", rebate_accrued: "账期回水",
rebate_in_bill: "回水已入账单", rebate_in_bill: "回水已入账单",
@@ -108,6 +108,7 @@ type WalletLogsBlockProps = {
title?: string; title?: string;
/** 信用盘:专用筛选项与列表文案 */ /** 信用盘:专用筛选项与列表文案 */
creditMode?: boolean; creditMode?: boolean;
settlementTimeZone?: string;
}; };
/** 类型筛选 + 列表(钱包盘待对账见钱包页横幅) */ /** 类型筛选 + 列表(钱包盘待对账见钱包页横幅) */
@@ -123,6 +124,7 @@ export function WalletLogsBlock({
currency, currency,
title, title,
creditMode = false, creditMode = false,
settlementTimeZone,
}: WalletLogsBlockProps) { }: WalletLogsBlockProps) {
const { t } = useTranslation("player"); const { t } = useTranslation("player");
const resolvedTitle = const resolvedTitle =
@@ -189,6 +191,7 @@ export function WalletLogsBlock({
item={row} item={row}
currency={currency} currency={currency}
creditMode={creditMode} creditMode={creditMode}
settlementTimeZone={settlementTimeZone}
/> />
)) ))
)} )}
@@ -226,14 +229,27 @@ export function LogRow({
item, item,
currency, currency,
creditMode = false, creditMode = false,
settlementTimeZone,
}: { }: {
item: WalletLogItem; item: WalletLogItem;
currency: string; currency: string;
creditMode?: boolean; creditMode?: boolean;
settlementTimeZone?: string;
}) { }) {
const { t } = useTranslation("player"); const { t } = useTranslation("player");
const ccy = item.currency_code || currency; const ccy = item.currency_code || currency;
const isIn = item.direction === "in"; const isIn = item.direction === "in";
const isCreditSettlementConfirm = creditMode && item.biz_type === "settlement_confirm";
const amountTone = isCreditSettlementConfirm
? "text-[#32518d]"
: isIn
? "text-emerald-600"
: "text-[#e5002c]";
const dotTone = isCreditSettlementConfirm
? "bg-[#2f6fca]"
: isIn
? "bg-emerald-500"
: "bg-[#e5002c]";
const statusTone = item.status === "posted" const statusTone = item.status === "posted"
? "border-emerald-200 bg-emerald-50 text-emerald-700" ? "border-emerald-200 bg-emerald-50 text-emerald-700"
: item.status === "pending_reconcile" : item.status === "pending_reconcile"
@@ -247,13 +263,15 @@ export function LogRow({
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<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={`size-2.5 rounded-full ${dotTone}`} 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, item.biz_type)} {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">
{formatPlayerInstant(item.created_at)} {creditMode && settlementTimeZone
? formatLotteryInstantInTimeZone(item.created_at, settlementTimeZone)
: formatPlayerInstant(item.created_at)}
</p> </p>
{item.ref_id ? ( {item.ref_id ? (
<p className="mt-1 truncate font-mono text-[11px] text-slate-400"> <p className="mt-1 truncate font-mono text-[11px] text-slate-400">
@@ -263,9 +281,13 @@ export function LogRow({
</div> </div>
<div className="flex shrink-0 flex-col items-end gap-2 text-right"> <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-[#e5002c]"}> <p className={`text-lg font-black tabular-nums ${amountTone}`}>
{isIn ? "+" : ""} {isCreditSettlementConfirm
{formatMinorAsCurrency(item.amount_abs, ccy)} ? t("wallet.creditReleasedAmount", {
defaultValue: "释额 {{amount}}",
amount: formatMinorAsCurrency(item.amount_abs, ccy),
})
: `${isIn ? "+" : ""}${formatMinorAsCurrency(item.amount_abs, ccy)}`}
</p> </p>
<span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-black ${statusTone}`}> <span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-black ${statusTone}`}>
{txnStatusLabel(item.status, t)} {txnStatusLabel(item.status, t)}

View File

@@ -19,7 +19,7 @@ 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 { formatLotteryInstantInTimeZone, 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";
@@ -69,18 +69,31 @@ function settlementStatusLabel(
return t(`wallet.settlementStatus.${status}`, { defaultValue: fallback[status] ?? status }); return t(`wallet.settlementStatus.${status}`, { defaultValue: fallback[status] ?? status });
} }
function formatPeriodRange(item: WalletSettlementBillItem): string { function formatPeriodRange(
const start = item.period_start ? formatPlayerInstant(item.period_start).slice(0, 10) : "—"; item: WalletSettlementBillItem,
const end = item.period_end ? formatPlayerInstant(item.period_end).slice(0, 10) : "—"; 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}`; return `${start} / ${end}`;
} }
function CreditSettlementBillsBlock({ function CreditSettlementBillsBlock({
data, data,
currency, currency,
settlementTimeZone,
}: { }: {
data: WalletSettlementBillsData | null; data: WalletSettlementBillsData | null;
currency: string; currency: string;
settlementTimeZone?: string;
}) { }) {
const { t } = useTranslation("player"); const { t } = useTranslation("player");
const summary = data?.summary; const summary = data?.summary;
@@ -157,7 +170,9 @@ function CreditSettlementBillsBlock({
? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" }) ? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" })
: t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })} : t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })}
</p> </p>
<p className="mt-0.5 text-[11px] text-slate-500">{formatPeriodRange(item)}</p> <p className="mt-0.5 text-[11px] text-slate-500">
{formatPeriodRange(item, settlementTimeZone)}
</p>
</div> </div>
</div> </div>
<div className="shrink-0 text-right"> <div className="shrink-0 text-right">
@@ -199,6 +214,7 @@ export function WalletScreen() {
const sectionDeepLinkHandledRef = useRef(false); const sectionDeepLinkHandledRef = useRef(false);
const profile = usePlayerSessionStore((s) => s.profile); const profile = usePlayerSessionStore((s) => s.profile);
const settlementTimeZone = profile?.settlement_timezone || "UTC";
const fundingModeView = resolveFundingModeView(balance, profile); const fundingModeView = resolveFundingModeView(balance, profile);
const fundingModeKnown = fundingModeView !== "unknown"; const fundingModeKnown = fundingModeView !== "unknown";
const isCreditPlayer = fundingModeView === "credit"; const isCreditPlayer = fundingModeView === "credit";
@@ -478,7 +494,11 @@ export function WalletScreen() {
</section> </section>
{fundingModeKnown && isCreditPlayer ? ( {fundingModeKnown && isCreditPlayer ? (
<CreditSettlementBillsBlock data={settlementBills} currency={currency} /> <CreditSettlementBillsBlock
data={settlementBills}
currency={currency}
settlementTimeZone={settlementTimeZone}
/>
) : null} ) : null}
{fundingModeKnown && !isCreditPlayer ? ( {fundingModeKnown && !isCreditPlayer ? (
@@ -562,6 +582,7 @@ export function WalletScreen() {
filter={filter} filter={filter}
onFilterChange={handleFilterChange} onFilterChange={handleFilterChange}
currency={currency} currency={currency}
settlementTimeZone={isCreditPlayer ? settlementTimeZone : undefined}
/> />
) : ( ) : (
<Skeleton className="h-40 w-full rounded-lg" /> <Skeleton className="h-40 w-full rounded-lg" />

View File

@@ -451,6 +451,7 @@
"wallet": "主站钱包玩家(划转与钱包余额流水)" "wallet": "主站钱包玩家(划转与钱包余额流水)"
}, },
"creditAvailableAfter": "变更后可用信用", "creditAvailableAfter": "变更后可用信用",
"creditReleasedAmount": "释额 {{amount}}",
"creditPaymentRecordOnly": "账期收付记账,不计入可用信用", "creditPaymentRecordOnly": "账期收付记账,不计入可用信用",
"totalRecords": "共 {{total}} 条记录", "totalRecords": "共 {{total}} 条记录",
"emptyLogs": "暂无流水", "emptyLogs": "暂无流水",
@@ -501,7 +502,7 @@
"game_settlement_loss": "亏损入账", "game_settlement_loss": "亏损入账",
"game_settlement_win": "中奖释额", "game_settlement_win": "中奖释额",
"game_settlement_reversal": "结算冲正", "game_settlement_reversal": "结算冲正",
"settlement_confirm": "账期收款", "settlement_confirm": "账期还款释额",
"settlement_payout": "账期付款", "settlement_payout": "账期付款",
"rebate_accrued": "账期回水", "rebate_accrued": "账期回水",
"rebate_in_bill": "回水已入账单", "rebate_in_bill": "回水已入账单",

View File

@@ -7,6 +7,22 @@ export type DrawResultsNumbers = {
consolation: string[]; consolation: string[];
}; };
export type DrawResultProviderRow = {
provider_code?: string | null;
provider_name?: string | null;
result_version: number;
results: DrawResultsNumbers;
result_items: Array<{
prize_type: string;
prize_index: number;
number_4d: string;
suffix_3d: string | null;
suffix_2d: string | null;
head_digit: number | null;
tail_digit: number | null;
}>;
};
export type DrawResultListItem = { export type DrawResultListItem = {
draw_id: string; draw_id: string;
draw_no: string; draw_no: string;
@@ -15,6 +31,8 @@ export type DrawResultListItem = {
draw_time_iso?: string | null; draw_time_iso?: string | null;
result_version: number; result_version: number;
result_source: string | null; result_source: string | null;
provider_code?: string | null;
provider_name?: string | null;
jackpot?: { jackpot?: {
currency_code: string; currency_code: string;
enabled: boolean; enabled: boolean;
@@ -33,6 +51,7 @@ export type DrawResultListItem = {
head_digit: number | null; head_digit: number | null;
tail_digit: number | null; tail_digit: number | null;
}>; }>;
provider_results?: DrawResultProviderRow[];
}; };
export type DrawResultsListPayload = { export type DrawResultsListPayload = {

View File

@@ -8,6 +8,7 @@ export type PlayerMeData = {
username: string | null; username: string | null;
nickname: string | null; nickname: string | null;
default_currency: string; default_currency: string;
settlement_timezone: string;
status: number; status: number;
/** 当次请求解析后的界面语言zh / en / ne */ /** 当次请求解析后的界面语言zh / en / ne */
locale: string; locale: string;