From 56ba12fbfce7fbb9cdd110eb3e4fec167b5fa792 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 9 Jul 2026 15:48:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor(hall):=20=E4=BC=98=E5=8C=96=E6=8A=95?= =?UTF-8?q?=E6=B3=A8=E6=A0=BC=E5=AD=90=E8=A1=8C=E6=95=B0=E4=B8=8E=E5=8F=B7?= =?UTF-8?q?=E7=A0=81=E8=BE=93=E5=85=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入默认草稿行数常量及批量生成草稿行函数 - 根据玩法类别调整号码最大输入长度和清洗规则 - 更新号码输入组件以适应动态最大长度和格式规范 - 修正相关回调依赖,确保玩法类别变更时生效 - 改善行状态、占位符和样式传递,增强代码可读性 feat(results): 添加多提供商开奖结果切换功能 - 新增状态管理并展示多提供商按钮切换激活项 - 根据选中的提供商动态展示对应开奖结果数据 - 细化UI显示,支持多提供商名称并列渲染 feat(results): 在结果列表中显示多提供商名称 - 当存在多个提供商结果时展示其名称或代码 - 改善界面信息丰富度,方便用户辨识来源 feat(wallet): 支持账期还款释额类型的流水显示 - 调整账期流水类型文案,精准反映释额含义 - 引入账期时区参数以支持多时区时间格式化 - 自定义信用盘账期释额流水的颜色和文案显示 - 在流水明细和列表中根据时区格式化展示时间 feat(wallet): 支持钱包账期账单区间时区格式化 - 允许传入账期时区参数格式化账单区间时间 - 在账期账单区间显示组件中显示对应时区格式 - 根据玩家配置自动获取并传递账期时区信息 - 条件渲染中信用盘区块支持账期时区参数传递 chore(i18n): 更新中文钱包模块多处文案 - 新增“释额 {{amount}}”等账期释额相关文案 - 修改“账期收款”为“账期还款释额”以符合业务描述 refactor(types): 扩展开奖结果支持多提供商结构 - 新增DrawResultProviderRow类型定义以描述提供商结果 - 在DrawResultListItem中增加provider_results字段支持多结果 - 在玩家信息类型中新增settlement_timezone字段表示账期时区 --- src/features/hall/hall-betting-grid.tsx | 79 ++++++++++++------- .../results/draw-result-detail-screen.tsx | 35 +++++++- .../results/draw-results-list-screen.tsx | 10 +++ src/features/wallet/wallet-logs-block.tsx | 36 +++++++-- src/features/wallet/wallet-screen.tsx | 33 ++++++-- src/i18n/locales/zh/player.json | 3 +- src/types/api/draw-results.ts | 19 +++++ src/types/api/player-me.ts | 1 + 8 files changed, 173 insertions(+), 43 deletions(-) diff --git a/src/features/hall/hall-betting-grid.tsx b/src/features/hall/hall-betting-grid.tsx index 7ab067d..8f7bd9e 100644 --- a/src/features/hall/hall-betting-grid.tsx +++ b/src/features/hall/hall-betting-grid.tsx @@ -150,6 +150,11 @@ const D4_PLAY_ORDER = [ "digit_small", ] 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 { const id = @@ -215,8 +220,18 @@ function playColumnsForCategory( }); } -function sanitizeNumber(raw: string): string { - return raw.replace(/[^0-9Rr]/g, "").toUpperCase().slice(0, 4); +function numberMaxCharsForCategory(category: PlayHallCategory): number { + 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 { @@ -439,7 +454,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile)); const isMobile = useIsMobile(); - const [rows, setRows] = useState(() => Array.from({ length: 20 }, newDraftRow)); + const [rows, setRows] = useState(() => newDraftRows()); const [activeRowId, setActiveRowId] = useState(null); const [catalogState, setCatalogState] = useState< | { kind: "loading" } @@ -642,15 +657,16 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status)); const numberPlaceholder = activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000"; + const numberMaxChars = numberMaxCharsForCategory(activeCategory); const updateRowNumber = useCallback((id: string, value: string) => { setRows((current) => current.map((row) => - row.id === id ? { ...row, number: sanitizeNumber(value) } : row, + row.id === id ? { ...row, number: sanitizeNumber(value, activeCategory) } : row, ), ); setActiveRowId(id); - }, []); + }, [activeCategory]); const updateAmount = useCallback((rowId: string, playCode: string, value: string) => { setRows((current) => @@ -1151,7 +1167,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } setPreviewData(null); setResultData(data); setResultOpen(true); - setRows([newDraftRow()]); + setRows(newDraftRows()); setActiveRowId(null); triggerWalletPollingAfterBet(); void refreshWallet(); @@ -1752,6 +1768,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } rowActive={activeRowId === row.id} tableDisabled={tableDisabled} numberPlaceholder={numberPlaceholder} + activeCategory={activeCategory} + numberMaxChars={numberMaxChars} playColumns={playColumns} alertRows={alertRows} liveSoldOutNumbers={liveSoldOutNumbers} @@ -1905,6 +1923,8 @@ const DraftRowItem = memo(function DraftRowItem({ rowActive, tableDisabled, numberPlaceholder, + activeCategory, + numberMaxChars, playColumns, alertRows, liveSoldOutNumbers, @@ -1919,16 +1939,18 @@ const DraftRowItem = memo(function DraftRowItem({ indexColClass, numberColClass, stickyNumberLeftClass, -}: { - row: DraftRow; - index: number; - rowActive: boolean; - tableDisabled: boolean; - numberPlaceholder: string; - playColumns: PlayColumn[]; - alertRows: DrawCurrentRiskPoolAlert[]; - liveSoldOutNumbers: Set; - liveWarningNumbers: Set; + }: { + row: DraftRow; + index: number; + rowActive: boolean; + tableDisabled: boolean; + numberPlaceholder: string; + activeCategory: PlayHallCategory; + numberMaxChars: number; + playColumns: PlayColumn[]; + alertRows: DrawCurrentRiskPoolAlert[]; + liveSoldOutNumbers: Set; + liveWarningNumbers: Set; updateRowNumber: (id: string, value: string) => void; updateAmount: (rowId: string, playCode: string, value: string) => void; setActiveRowId: (id: string) => void; @@ -1937,10 +1959,12 @@ const DraftRowItem = memo(function DraftRowItem({ isMobile: boolean; removable: boolean; indexColClass: string; - numberColClass: string; - stickyNumberLeftClass: string; -}) { - return ( + numberColClass: string; + stickyNumberLeftClass: string; + }) { + const displayNumber = sanitizeNumber(row.number, activeCategory); + + return ( - setActiveRowId(row.id)} onClick={() => setActiveRowId(row.id)} onChange={(event) => updateRowNumber(row.id, event.target.value)} diff --git a/src/features/results/draw-result-detail-screen.tsx b/src/features/results/draw-result-detail-screen.tsx index 4ee40f2..4a06c04 100644 --- a/src/features/results/draw-result-detail-screen.tsx +++ b/src/features/results/draw-result-detail-screen.tsx @@ -47,6 +47,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); + const [activeProviderCode, setActiveProviderCode] = useState(""); const [highlightSet, setHighlightSet] = useState | null>(null); const [myTotals, setMyTotals] = useState<{ win: number; @@ -60,6 +61,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) try { const row = await getDrawResultByNo(drawNo, { currency: activeCurrency }); setData(row); + setActiveProviderCode(String(row.provider_code ?? row.provider_results?.[0]?.provider_code ?? "")); } catch { setData(null); setError(t("results.unavailable")); @@ -144,6 +146,12 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) } 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 = myTotals && myTotals.hasBets && @@ -222,6 +230,29 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) {t("results.detail")} + {providerResults.length > 1 ? ( +
+ {providerResults.map((row) => { + const code = String(row.provider_code ?? ""); + const active = code === String(activeProviderResult?.provider_code ?? ""); + return ( + + ); + })} +
+ ) : null}
{RESULTS_TOP_PRIZE_KEYS.map((tier) => (

- {data.results[tier]} + {activeResults[tier]}

))} @@ -240,7 +271,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
diff --git a/src/features/results/draw-results-list-screen.tsx b/src/features/results/draw-results-list-screen.tsx index 6b4ee13..37c7a37 100644 --- a/src/features/results/draw-results-list-screen.tsx +++ b/src/features/results/draw-results-list-screen.tsx @@ -252,6 +252,11 @@ export function DrawResultsListScreen() { ), })}

+ {(featured.provider_results?.length ?? 0) > 1 ? ( +

+ {featured.provider_results?.map((row) => row.provider_name || row.provider_code).filter(Boolean).join(" / ")} +

+ ) : null} {formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)}

+ {(row.provider_results?.length ?? 0) > 1 ? ( +

+ {row.provider_results?.map((item) => item.provider_name || item.provider_code).filter(Boolean).join(" / ")} +

+ ) : null} {t("results.detail")} diff --git a/src/features/wallet/wallet-logs-block.tsx b/src/features/wallet/wallet-logs-block.tsx index 71df835..c11747d 100644 --- a/src/features/wallet/wallet-logs-block.tsx +++ b/src/features/wallet/wallet-logs-block.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; 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 type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs"; @@ -54,7 +54,7 @@ const CREDIT_BIZ_LABEL_FALLBACKS: Record = { game_settlement_loss: "亏损入账", game_settlement_win: "中奖释额", game_settlement_reversal: "结算冲正", - settlement_confirm: "账期收款", + settlement_confirm: "账期还款释额", settlement_payout: "账期付款", rebate_accrued: "账期回水", rebate_in_bill: "回水已入账单", @@ -108,6 +108,7 @@ type WalletLogsBlockProps = { title?: string; /** 信用盘:专用筛选项与列表文案 */ creditMode?: boolean; + settlementTimeZone?: string; }; /** 类型筛选 + 列表(钱包盘待对账见钱包页横幅) */ @@ -123,6 +124,7 @@ export function WalletLogsBlock({ currency, title, creditMode = false, + settlementTimeZone, }: WalletLogsBlockProps) { const { t } = useTranslation("player"); const resolvedTitle = @@ -189,6 +191,7 @@ export function WalletLogsBlock({ item={row} currency={currency} creditMode={creditMode} + settlementTimeZone={settlementTimeZone} /> )) )} @@ -226,14 +229,27 @@ export function LogRow({ item, currency, creditMode = false, + settlementTimeZone, }: { item: WalletLogItem; currency: string; creditMode?: boolean; + settlementTimeZone?: string; }) { const { t } = useTranslation("player"); const ccy = item.currency_code || currency; 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" ? "border-emerald-200 bg-emerald-50 text-emerald-700" : item.status === "pending_reconcile" @@ -247,13 +263,15 @@ export function LogRow({
- +

{logTypeLabel(item.type, t, creditMode, item.biz_type)}

- {formatPlayerInstant(item.created_at)} + {creditMode && settlementTimeZone + ? formatLotteryInstantInTimeZone(item.created_at, settlementTimeZone) + : formatPlayerInstant(item.created_at)}

{item.ref_id ? (

@@ -263,9 +281,13 @@ export function LogRow({

-

- {isIn ? "+" : "−"} - {formatMinorAsCurrency(item.amount_abs, ccy)} +

+ {isCreditSettlementConfirm + ? t("wallet.creditReleasedAmount", { + defaultValue: "释额 {{amount}}", + amount: formatMinorAsCurrency(item.amount_abs, ccy), + }) + : `${isIn ? "+" : "−"}${formatMinorAsCurrency(item.amount_abs, ccy)}`}

{txnStatusLabel(item.status, t)} diff --git a/src/features/wallet/wallet-screen.tsx b/src/features/wallet/wallet-screen.tsx index 8b811db..b4337eb 100644 --- a/src/features/wallet/wallet-screen.tsx +++ b/src/features/wallet/wallet-screen.tsx @@ -19,7 +19,7 @@ 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 { formatLotteryInstantInTimeZone, 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"; @@ -69,18 +69,31 @@ function settlementStatusLabel( 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) : "—"; +function formatPeriodRange( + item: WalletSettlementBillItem, + 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}`; } function CreditSettlementBillsBlock({ data, currency, + settlementTimeZone, }: { data: WalletSettlementBillsData | null; currency: string; + settlementTimeZone?: string; }) { const { t } = useTranslation("player"); const summary = data?.summary; @@ -157,7 +170,9 @@ function CreditSettlementBillsBlock({ ? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" }) : t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })}

-

{formatPeriodRange(item)}

+

+ {formatPeriodRange(item, settlementTimeZone)} +

@@ -199,6 +214,7 @@ export function WalletScreen() { const sectionDeepLinkHandledRef = useRef(false); const profile = usePlayerSessionStore((s) => s.profile); + const settlementTimeZone = profile?.settlement_timezone || "UTC"; const fundingModeView = resolveFundingModeView(balance, profile); const fundingModeKnown = fundingModeView !== "unknown"; const isCreditPlayer = fundingModeView === "credit"; @@ -478,7 +494,11 @@ export function WalletScreen() { {fundingModeKnown && isCreditPlayer ? ( - + ) : null} {fundingModeKnown && !isCreditPlayer ? ( @@ -562,6 +582,7 @@ export function WalletScreen() { filter={filter} onFilterChange={handleFilterChange} currency={currency} + settlementTimeZone={isCreditPlayer ? settlementTimeZone : undefined} /> ) : ( diff --git a/src/i18n/locales/zh/player.json b/src/i18n/locales/zh/player.json index 01d36e5..199cdd0 100644 --- a/src/i18n/locales/zh/player.json +++ b/src/i18n/locales/zh/player.json @@ -451,6 +451,7 @@ "wallet": "主站钱包玩家(划转与钱包余额流水)" }, "creditAvailableAfter": "变更后可用信用", + "creditReleasedAmount": "释额 {{amount}}", "creditPaymentRecordOnly": "账期收付记账,不计入可用信用", "totalRecords": "共 {{total}} 条记录", "emptyLogs": "暂无流水", @@ -501,7 +502,7 @@ "game_settlement_loss": "亏损入账", "game_settlement_win": "中奖释额", "game_settlement_reversal": "结算冲正", - "settlement_confirm": "账期收款", + "settlement_confirm": "账期还款释额", "settlement_payout": "账期付款", "rebate_accrued": "账期回水", "rebate_in_bill": "回水已入账单", diff --git a/src/types/api/draw-results.ts b/src/types/api/draw-results.ts index 79b4680..eeed5b8 100644 --- a/src/types/api/draw-results.ts +++ b/src/types/api/draw-results.ts @@ -7,6 +7,22 @@ export type DrawResultsNumbers = { 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 = { draw_id: string; draw_no: string; @@ -15,6 +31,8 @@ export type DrawResultListItem = { draw_time_iso?: string | null; result_version: number; result_source: string | null; + provider_code?: string | null; + provider_name?: string | null; jackpot?: { currency_code: string; enabled: boolean; @@ -33,6 +51,7 @@ export type DrawResultListItem = { head_digit: number | null; tail_digit: number | null; }>; + provider_results?: DrawResultProviderRow[]; }; export type DrawResultsListPayload = { diff --git a/src/types/api/player-me.ts b/src/types/api/player-me.ts index 2585feb..814398c 100644 --- a/src/types/api/player-me.ts +++ b/src/types/api/player-me.ts @@ -8,6 +8,7 @@ export type PlayerMeData = { username: string | null; nickname: string | null; default_currency: string; + settlement_timezone: string; status: number; /** 当次请求解析后的界面语言(zh / en / ne) */ locale: string;