diff --git a/src/features/draw/draw-status-meta.ts b/src/features/draw/draw-status-meta.ts index 2b609fd..4303a82 100644 --- a/src/features/draw/draw-status-meta.ts +++ b/src/features/draw/draw-status-meta.ts @@ -19,6 +19,26 @@ export function isHallBlockedForBetting(status: string): boolean { return status !== "open"; } +/** 已到计划开奖时刻、但调度尚未把期号推进到冷静期(closed → drawing → cooldown) */ +export function isHallAwaitingDrawProcessing( + status: string, + drawTimeIso: string | null | undefined, + secondsToDraw: number | null | undefined, + nowMs: number, +): boolean { + if (!["closing", "closed", "drawing", "review"].includes(status)) { + return false; + } + if (secondsToDraw !== null && secondsToDraw !== undefined && secondsToDraw <= 0) { + return true; + } + if (!drawTimeIso) { + return false; + } + const drawMs = Date.parse(drawTimeIso); + return !Number.isNaN(drawMs) && drawMs <= nowMs; +} + /** 对齐界面文档 §4.2 状态文案与 PRD 期号状态 */ export function drawStatusHud(status: string): DrawStatusHud { switch (status) { diff --git a/src/features/hall/hall-bet-rules.ts b/src/features/hall/hall-bet-rules.ts index 9b0f602..08aba12 100644 --- a/src/features/hall/hall-bet-rules.ts +++ b/src/features/hall/hall-bet-rules.ts @@ -46,6 +46,57 @@ export function playNeedsDigitSlot(playCode: string): boolean { return playCode === "digit_big" || playCode === "digit_small"; } +/** 与后端 {@link App\Services\Ticket\NumberNormalizer::normalizeRoll} 一致:4 位且至少含一个 R。 */ +export function isValidRollNumber(value: string): boolean { + const trimmed = value.trim().toUpperCase(); + return ( + trimmed.length === 4 + && /^[0-9R]+$/.test(trimmed) + && trimmed.includes("R") + ); +} + +export type DraftLineIssueReason = "invalid_number_length" | "roll_requires_r" | "missing_digit_slot"; + +/** + * 某玩法列已填金额但无法组成合法注单行时返回原因;合法则返回 null。 + */ +export function draftLineIssueReason( + playCode: string, + displayNumber: string, + digitSlot?: number, +): DraftLineIssueReason | null { + const normalized = normalizeNumberForPlay(displayNumber, playCode); + const spec = ticketNumberSpec(playCode); + if (normalized.length !== spec.maxChars) { + return "invalid_number_length"; + } + if (playCode === "roll" && !isValidRollNumber(normalized)) { + return "roll_requires_r"; + } + if (playNeedsDigitSlot(playCode) && digitSlot === undefined) { + return "missing_digit_slot"; + } + + return null; +} + +function normalizeNumberForPlay(number: string, playCode: string): string { + if (playCode.startsWith("pos_2")) return number.slice(-2); + if (playCode.startsWith("pos_3")) return number.slice(-3); + if ( + playCode === "head" + || playCode === "tail" + || playCode === "odd" + || playCode === "even" + || playCode === "digit_big" + || playCode === "digit_small" + ) { + return number.slice(-1); + } + return number.toUpperCase(); +} + /** 产品文档:iBox/Roll 单注金额;mBox 总金额摊分 */ export function ticketAmountHint( playCode: string, diff --git a/src/features/hall/hall-betting-grid.tsx b/src/features/hall/hall-betting-grid.tsx index d1da814..388ae6d 100644 --- a/src/features/hall/hall-betting-grid.tsx +++ b/src/features/hall/hall-betting-grid.tsx @@ -16,9 +16,11 @@ import { HallBetPreviewDialog } from "@/features/hall/hall-bet-preview-dialog"; import { HallBetResultDialog } from "@/features/hall/hall-bet-result-dialog"; import { mapTicketBetError } from "@/features/hall/hall-bet-errors"; import { + draftLineIssueReason, playNeedsDigitSlot, playNeedsDimension, ticketNumberSpec, + type DraftLineIssueReason, } from "@/features/hall/hall-bet-rules"; import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; @@ -26,6 +28,7 @@ import { triggerWalletPollingAfterBet } from "@/hooks/use-wallet-polling"; import { getLotteryEcho } from "@/lib/lottery-echo"; import { getLotteryRequestLocale } from "@/lib/lottery-locale"; import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money"; +import { playLabel } from "@/lib/play-labels"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; @@ -55,6 +58,12 @@ type DraftEntry = { line: TicketLineInput; }; +type DraftLineIssue = { + rowNo: number; + playCode: string; + reason: DraftLineIssueReason; +}; + type PlayColumn = { key: string; play: PlayEffectivePlayRow; @@ -132,8 +141,20 @@ function isPlayOpenForPlayer(row: PlayEffectivePlayRow): boolean { return Boolean(row.master_enabled && row.config?.is_enabled); } -function pickDisplayName(row: PlayEffectivePlayRow): string { - return row.display_name?.trim() || row.play_code; +type HallTranslate = (key: string, options?: Record) => string; + +/** 表头用短标签,避免 digit_big + 千/百/十/个 挤成一团。 */ +function playColumnHeaderLabel( + play: PlayEffectivePlayRow, + category: Exclude, + digitSlot: number | undefined, + t: HallTranslate, +): string { + if (digitSlot !== undefined) { + const kind = play.play_code === "digit_big" ? "big" : "small"; + return `${t(`hall.table.digitShort.${kind}`)}·${digitSlotLabel(category, digitSlot)}`; + } + return playLabel(play.play_code, t); } function digitSlotOptions(category: Exclude): number[] { @@ -226,6 +247,9 @@ function lineForPlay( digitSlot?: number, ): TicketLineInput | null { const number = normalizeNumberForPlay(displayNumber, play.play_code); + if (draftLineIssueReason(play.play_code, displayNumber, digitSlot) !== null) { + return null; + } const spec = ticketNumberSpec(play.play_code); if (number.length !== spec.maxChars) { return null; @@ -499,6 +523,16 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } return playColumnsForCategory(categoryPlays, activeCategory); }, [activeCategory, categoryPlays]); + const tableMinWidthPx = useMemo(() => { + const indexCol = 40; + const numberCol = activeCategory === "D4" ? 112 : activeCategory === "D3" ? 88 : 72; + const amountCol = 72; + const deleteCol = 36; + return indexCol + numberCol + playColumns.length * amountCol + deleteCol; + }, [activeCategory, playColumns.length]); + + const showWideTableHint = activeCategory === "D4" && playColumns.length > 8; + const activeRow = useMemo( () => rows.find((row) => row.id === activeRowId) ?? rows[0] ?? null, [activeRowId, rows], @@ -535,7 +569,27 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } const tableDisabled = !isBettable || catalogState.kind !== "ok"; const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status)); - const numberPlaceholder = activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000"; + const defaultNumberPlaceholder = + activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000"; + const numberPlaceholder = useMemo(() => { + if (activeCategory !== "D4") return defaultNumberPlaceholder; + const targetRow = activeRow ?? rows[0]; + if (!targetRow) return defaultNumberPlaceholder; + const hasRollStake = playColumns.some((column) => { + if (column.play.play_code !== "roll") return false; + const amount = parseDecimalInputToMinor(targetRow.amounts[column.key] ?? "", currencyCode); + return amount !== null && amount > 0; + }); + return hasRollStake ? t("hall.numberInput.rollPlaceholder") : defaultNumberPlaceholder; + }, [ + activeCategory, + activeRow, + currencyCode, + defaultNumberPlaceholder, + playColumns, + rows, + t, + ]); const updateRowNumber = (id: string, value: string) => { setRows((current) => @@ -702,6 +756,34 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } }; }, [clearAmountsForPlay, drawNo, loadCatalog, reloadDraw, t]); + const collectDraftLineIssues = useCallback((): DraftLineIssue[] => { + if (activeCategory === "JACKPOT") return []; + const issues: DraftLineIssue[] = []; + rows.forEach((row, rowIndex) => { + playColumns.forEach((column) => { + const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode); + if (amount === null || amount <= 0) return; + const reason = draftLineIssueReason(column.play.play_code, row.number, column.digitSlot); + if (reason !== null) { + issues.push({ + rowNo: rowIndex + 1, + playCode: column.play.play_code, + reason, + }); + } + }); + }); + return issues; + }, [activeCategory, currencyCode, playColumns, rows]); + + const formatDraftLineIssue = useCallback( + (issue: DraftLineIssue): string => { + const label = playLabel(issue.playCode, t); + return t(`hall.lineIssue.${issue.reason}`, { row: issue.rowNo, play: label }); + }, + [t], + ); + const collectEntries = useCallback((): DraftEntry[] => { if (activeCategory === "JACKPOT") return []; const entries: DraftEntry[] = []; @@ -798,6 +880,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } return; } + const lineIssues = collectDraftLineIssues(); + if (lineIssues.length > 0) { + toast.error(formatDraftLineIssue(lineIssues[0])); + return; + } + const lines = buildLines(); if (lines.length === 0) { toast.error(t("hall.emptyLines")); @@ -842,6 +930,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } return; } + const lineIssues = collectDraftLineIssues(); + if (lineIssues.length > 0) { + toast.error(formatDraftLineIssue(lineIssues[0])); + return; + } + const lines = buildLines(); if (lines.length === 0) { toast.error(t("hall.changedBeforeSubmit")); @@ -1133,53 +1227,88 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } ) : null} + {showWideTableHint ? ( +

{t("hall.table.scrollHint")}

+ ) : null} +
-
+
- - {playColumns.map((column) => ( - ))} - {rows.map((row, index) => { const rowKey = row.id; + const rowActive = activeRowId === row.id; return ( - - + - {playColumns.map((column) => { @@ -1227,7 +1359,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } onClick={() => setActiveRowId(row.id)} onChange={(event) => updateAmount(row.id, column.key, event.target.value)} className={cn( - "h-8 rounded-md border-[#e1e8f3] bg-white px-1 text-center text-xs font-bold tabular-nums shadow-sm focus-visible:ring-[#1d57b7]", + "h-8 w-full rounded-md border-[#e1e8f3] bg-white px-1 text-center text-xs font-bold tabular-nums shadow-sm focus-visible:ring-[#1d57b7]", hasAmount && "border-[#9bbcff] bg-[#f5f9ff] text-[#0b3f96]", status === "warning" && "border-amber-200 bg-amber-50 text-amber-800", status === "sold_out" && "border-slate-200 bg-slate-100 text-slate-400", diff --git a/src/features/hall/hall-draw-panel.tsx b/src/features/hall/hall-draw-panel.tsx index 7daac4c..53e2f0b 100644 --- a/src/features/hall/hall-draw-panel.tsx +++ b/src/features/hall/hall-draw-panel.tsx @@ -1,12 +1,16 @@ "use client"; import { Hourglass, Landmark, TimerReset } from "lucide-react"; -import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; -import { drawStatusHud, isHallBlockedForBetting, isHallSealedCountdownUi } from "@/features/draw/draw-status-meta"; +import { + drawStatusHud, + isHallAwaitingDrawProcessing, + isHallBlockedForBetting, + isHallSealedCountdownUi, +} from "@/features/draw/draw-status-meta"; import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live"; import { formatSecondsClock } from "@/lib/format-gmt"; import { formatLotteryInstant } from "@/lib/player-datetime"; @@ -39,47 +43,67 @@ function CurrentTime({ payload }: { payload: DrawCurrentPayload }) { } function CloseTime({ - serverNowMs, + nowMs, hud, payload, }: { - serverNowMs: number; + nowMs: number; hud: ReturnType; payload: DrawCurrentPayload; }) { const { t } = useTranslation("player"); const sealedCountdown = isHallSealedCountdownUi(payload.status); - const [elapsedSeconds, setElapsedSeconds] = useState(0); - - useEffect(() => { - const intervalId = window.setInterval(() => { - setElapsedSeconds((current) => current + 1); - }, 1000); - - return () => window.clearInterval(intervalId); - }, []); - - const nowMs = serverNowMs + elapsedSeconds * 1000; + const awaitingDraw = isHallAwaitingDrawProcessing( + payload.status, + payload.draw_time, + payload.seconds_to_draw, + nowMs, + ); let seconds = 0; let label = t("draw.closesIn"); + let showClock = true; - if (hud.countdownKind === "none") { + if (awaitingDraw) { + label = t("draw.drawProcessing"); + showClock = false; + } else if (hud.countdownKind === "none") { label = t(hud.labelKey, { defaultValue: hud.labelKey }); + showClock = false; } else if (hud.countdownKind === "close") { - seconds = Math.max(0, Math.ceil(((payload.close_time ? Date.parse(payload.close_time) : 0) - nowMs) / 1000)); + seconds = + payload.seconds_to_close != null + ? Math.max(0, payload.seconds_to_close) + : Math.max( + 0, + Math.ceil(((payload.close_time ? Date.parse(payload.close_time) : 0) - nowMs) / 1000), + ); } else if (hud.countdownKind === "draw") { - seconds = Math.max(0, Math.ceil(((payload.draw_time ? Date.parse(payload.draw_time) : 0) - nowMs) / 1000)); + seconds = + payload.seconds_to_draw != null + ? Math.max(0, payload.seconds_to_draw) + : Math.max( + 0, + Math.ceil(((payload.draw_time ? Date.parse(payload.draw_time) : 0) - nowMs) / 1000), + ); label = sealedCountdown ? t("draw.drawsIn") : t("draw.closesIn"); } else if (hud.countdownKind === "cooldown") { - seconds = Math.max(0, Math.ceil(((payload.cooling_end_time ? Date.parse(payload.cooling_end_time) : 0) - nowMs) / 1000)); + seconds = + payload.seconds_remaining_in_cooldown != null + ? Math.max(0, payload.seconds_remaining_in_cooldown) + : Math.max( + 0, + Math.ceil( + ((payload.cooling_end_time ? Date.parse(payload.cooling_end_time) : 0) - nowMs) / 1000, + ), + ); label = t("draw.coolDown"); } return ( <> - {hud.countdownKind === "none" ? "--:--" : formatSecondsClock(seconds)} + {showClock ? formatSecondsClock(seconds) : "--:--"} {label} @@ -87,7 +111,7 @@ function CloseTime({ } export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) { - const { raw, display, serverNowMs, error, reload } = drawLive; + const { raw, display, error, reload } = drawLive; const { t } = useTranslation("player"); if (error) { @@ -152,8 +176,8 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
diff --git a/src/features/hall/use-hall-draw-live.ts b/src/features/hall/use-hall-draw-live.ts index 4262da5..aea9eaa 100644 --- a/src/features/hall/use-hall-draw-live.ts +++ b/src/features/hall/use-hall-draw-live.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { getDrawCurrent } from "@/api/draw"; -import { isHallSealedCountdownUi } from "@/features/draw/draw-status-meta"; +import { isHallAwaitingDrawProcessing, isHallSealedCountdownUi } from "@/features/draw/draw-status-meta"; import { getLotteryEcho } from "@/lib/lottery-echo"; import { useNetworkConnectionStore } from "@/stores/network-connection-store"; import type { DrawCurrentPayload, DrawCurrentResponse } from "@/types/api/draw-current"; @@ -13,6 +13,8 @@ export type HallDrawLiveSnapshot = { raw: DrawCurrentPayload | null | undefined; display: DrawCurrentPayload | null | undefined; serverNowMs: number; + /** 与 display 漂移推演一致的本地「当前」毫秒时间戳 */ + nowMs: number; error: string | null; reload: () => Promise; isBettable: boolean; @@ -266,6 +268,13 @@ export function useHallDrawLive(): HallDrawLiveSnapshot { ((display.seconds_remaining_in_cooldown ?? 1) === 0 || (coolingEndMs !== null && !Number.isNaN(coolingEndMs) && coolingEndMs <= nowMs)); + const awaitingDraw = isHallAwaitingDrawProcessing( + display.status, + display.draw_time, + display.seconds_to_draw, + nowMs, + ); + const sealedDone = isHallSealedCountdownUi(display.status) && (display.seconds_to_draw ?? 1) === 0; @@ -274,11 +283,13 @@ export function useHallDrawLive(): HallDrawLiveSnapshot { const trigger = coolingDone ? `${display.draw_no}:cooldown-end` - : sealedDone - ? `${display.draw_no}:sealed-end` - : closeDone - ? `${display.draw_no}:close-end` - : null; + : awaitingDraw + ? `${display.draw_no}:awaiting-draw` + : sealedDone + ? `${display.draw_no}:sealed-end` + : closeDone + ? `${display.draw_no}:close-end` + : null; if (trigger && zeroRefreshKeyRef.current !== trigger) { zeroRefreshKeyRef.current = trigger; @@ -292,13 +303,34 @@ export function useHallDrawLive(): HallDrawLiveSnapshot { } }, [display?.draw_no, display?.status]); - // WebSocket 已连接时的兜底轮询(tick 最多 1 分钟延迟时的保险) + const needsFastDrawPoll = + display != null + && isHallAwaitingDrawProcessing( + display.status, + display.draw_time, + display.seconds_to_draw, + nowMs, + ); + + // 封盘/待开奖/开奖中:调度推进状态前每 3 秒拉一次,避免 0:00 卡几十秒 useEffect(() => { + if (!needsFastDrawPoll) { + return; + } const intervalId = window.setInterval(() => { void load(); - }, 45_000); + }, 3000); return () => window.clearInterval(intervalId); - }, [load]); + }, [needsFastDrawPoll, load]); - return { raw, display, serverNowMs, error, reload: load, isBettable }; + // WebSocket 已连接时的兜底轮询(tick 延迟时的保险) + useEffect(() => { + const intervalMs = needsFastDrawPoll ? 15_000 : 45_000; + const intervalId = window.setInterval(() => { + void load(); + }, intervalMs); + return () => window.clearInterval(intervalId); + }, [load, needsFastDrawPoll]); + + return { raw, display, serverNowMs, nowMs, error, reload: load, isBettable }; } diff --git a/src/features/orders/ticket-item-status.tsx b/src/features/orders/ticket-item-status.tsx index 75ec375..398ace0 100644 --- a/src/features/orders/ticket-item-status.tsx +++ b/src/features/orders/ticket-item-status.tsx @@ -7,9 +7,15 @@ export function ticketStatusDisplay( t?: (key: string, options?: { defaultValue?: string; status?: string }) => string, ): { label: string; dotClass: string; ring?: boolean } { const total = winMinor + jackpotMinor; - if (status === "success" || status === "pending_draw") { + if ( + status === "pending_draw" || + status === "placed" || + status === "partial_failed" || + status === "pending_confirm" || + status === "partial_pending_confirm" + ) { return { - label: t?.(status === "pending_draw" ? "ticketStatus.pending_draw" : "ticketStatus.success") ?? status, + label: t?.("ticketStatus.pending_draw") ?? status, dotClass: "bg-sky-500", }; } diff --git a/src/features/orders/ticket-orders-list-screen.tsx b/src/features/orders/ticket-orders-list-screen.tsx index 532efcc..73afa83 100644 --- a/src/features/orders/ticket-orders-list-screen.tsx +++ b/src/features/orders/ticket-orders-list-screen.tsx @@ -30,7 +30,7 @@ import { cn } from "@/lib/utils"; import type { TicketItemListRow } from "@/types/api/ticket-items"; const ORDERS_PAGE_SIZE = 20; -const STATUS_OPTIONS = ["pending_draw", "success", "settled_win", "settled_lose", "failed"] as const; +const STATUS_OPTIONS = ["pending_draw", "pending_payout", "settled_win", "settled_lose", "failed"] as const; function parseYmd(value: string): Date | undefined { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); diff --git a/src/i18n/locales/en/player.json b/src/i18n/locales/en/player.json index 3cedf18..9f3c81d 100644 --- a/src/i18n/locales/en/player.json +++ b/src/i18n/locales/en/player.json @@ -75,6 +75,7 @@ "currentTime": "Current Time", "closesIn": "Closes In", "drawsIn": "Draws In", + "drawProcessing": "Drawing…", "coolDown": "Cool Down", "loadFailedRefresh": "Failed to load. Pull down to refresh.", "issueNo": "Issue No.", @@ -103,6 +104,11 @@ "notBettable": "This issue is closed or cannot accept bets.", "catalogNotReady": "Play configuration is still loading.", "emptyLines": "Please enter at least one valid number and stake amount.", + "lineIssue": { + "invalid_number_length": "Row {{row}} «{{play}}»: invalid number length. Check the number field.", + "roll_requires_r": "Row {{row}} «{{play}}»: use 4 characters with at least one R (e.g. 12R4, RR34). R marks rolling digits.", + "missing_digit_slot": "Row {{row}} «{{play}}»: missing digit slot. Refresh and try again." + }, "previewFailed": "Preview failed", "closedSubmit": "Closed. Cannot submit.", "changedBeforeSubmit": "Your draft changed before submission. Close the preview and try again.", @@ -177,6 +183,10 @@ "submitBet": "Submit Bet", "insufficientBalance": "Insufficient balance", "amountPlaceholder": "Amount", + "digitShort": { + "big": "B", + "small": "S" + }, "filledPlayCount": "{{count}} plays filled", "tapToFill": "Tap to enter amounts", "rowActual": "Actual", diff --git a/src/i18n/locales/ne/player.json b/src/i18n/locales/ne/player.json index f5e4588..778a621 100644 --- a/src/i18n/locales/ne/player.json +++ b/src/i18n/locales/ne/player.json @@ -75,6 +75,7 @@ "currentTime": "हालको समय", "closesIn": "बन्द हुन बाँकी", "drawsIn": "ड्र हुन बाँकी", + "drawProcessing": "ड्रअ प्रक्रियामा", "coolDown": "कुल डाउन", "loadFailedRefresh": "लोड असफल भयो। तल तानेर रिफ्रेस गर्नुहोस्।", "issueNo": "इश्यू नं.", @@ -103,6 +104,11 @@ "notBettable": "यो इश्यू बन्द छ वा बेट स्वीकार गर्न सक्दैन।", "catalogNotReady": "प्ले कन्फिगरेसन अझै लोड हुँदैछ।", "emptyLines": "कृपया कम्तीमा एक मान्य नम्बर र रकम प्रविष्ट गर्नुहोस्।", + "lineIssue": { + "invalid_number_length": "पङ्क्ति {{row}} «{{play}}»: नम्बरको लम्बाइ मिलेन। नम्बर क्षेत्र जाँच गर्नुहोस्।", + "roll_requires_r": "पङ्क्ति {{row}} «{{play}}»: 4 अक्षर र कम्तीमा एक R चाहिन्छ (जस्तै 12R4, RR34)। R = घुम्ने अंक।", + "missing_digit_slot": "पङ्क्ति {{row}} «{{play}}»: अंक स्थान छुट्यो। रिफ्रेस गरी पुनः प्रयास गर्नुहोस्।" + }, "previewFailed": "पूर्वावलोकन असफल", "closedSubmit": "बन्द भयो। पेश गर्न सकिँदैन।", "changedBeforeSubmit": "पेश गर्नु अघि ड्राफ्ट परिवर्तन भयो। पूर्वावलोकन बन्द गरी फेरि प्रयास गर्नुहोस्।", @@ -177,6 +183,10 @@ "submitBet": "बेट पेश गर्नुहोस्", "insufficientBalance": "ब्यालेन्स अपुग", "amountPlaceholder": "रकम", + "digitShort": { + "big": "ठू", + "small": "सा" + }, "filledPlayCount": "{{count}} प्ले भरियो", "tapToFill": "रकम लेख्न ट्याप गर्नुहोस्", "rowActual": "वास्तविक", diff --git a/src/i18n/locales/zh/player.json b/src/i18n/locales/zh/player.json index 1dd229a..233f58c 100644 --- a/src/i18n/locales/zh/player.json +++ b/src/i18n/locales/zh/player.json @@ -75,6 +75,7 @@ "currentTime": "当前时间", "closesIn": "距封盘", "drawsIn": "距开奖", + "drawProcessing": "开奖处理中", "coolDown": "冷静期", "loadFailedRefresh": "加载失败,请下拉刷新", "issueNo": "期号", @@ -103,6 +104,11 @@ "notBettable": "当前已封盘或不可下注。", "catalogNotReady": "玩法配置尚未加载完成。", "emptyLines": "请至少填写一组有效号码和下注金额。", + "lineIssue": { + "invalid_number_length": "第 {{row}} 行「{{play}}」号码位数不正确,请检查号码列。", + "roll_requires_r": "第 {{row}} 行「{{play}}」须为 4 位且含 R(如 12R4、RR34),R 表示滚动位。", + "missing_digit_slot": "第 {{row}} 行「{{play}}」缺少位数,请刷新后重试。" + }, "previewFailed": "预览失败", "closedSubmit": "已封盘,无法提交。", "changedBeforeSubmit": "提交前数据已变化,请关闭预览后重试。", @@ -177,6 +183,10 @@ "submitBet": "提交下注", "insufficientBalance": "余额不足", "amountPlaceholder": "金额", + "digitShort": { + "big": "大", + "small": "小" + }, "filledPlayCount": "已填写 {{count}} 个玩法", "tapToFill": "点击填写玩法金额", "rowActual": "实扣",
+ {t("hall.table.no", { defaultValue: "No." })} - {t("hall.table.number", { defaultValue: "Number" })} - ({numberPlaceholder}) + + {t("hall.table.number", { defaultValue: "Number" })} + + {numberPlaceholder} + - - {pickDisplayName(column.play)} - {column.digitSlot !== undefined && activeCategory !== "JACKPOT" - ? `-${digitSlotLabel(activeCategory, column.digitSlot)}` - : ""} + + + {playColumnHeaderLabel(column.play, activeCategory, column.digitSlot, t)} - + {t("hall.table.amountPlaceholder")} +
+
{index + 1} + setActiveRowId(row.id)} onClick={() => setActiveRowId(row.id)} onChange={(event) => updateRowNumber(row.id, event.target.value)} - className="h-8 rounded-md border-[#e1e8f3] bg-white px-1 text-center font-mono text-sm font-black tracking-[0.1em] text-slate-950 shadow-sm focus-visible:ring-[#1d57b7]" + className={cn( + "h-9 w-full rounded-md border-[#e1e8f3] bg-white px-2 text-center font-mono text-base font-bold tabular-nums text-slate-950 shadow-sm focus-visible:ring-[#1d57b7]", + activeCategory === "D4" && "tracking-[0.2em]", + )} />