From 51b2a36cc50ca53e3649b6f54cfb325cabe5d8b7 Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 26 May 2026 14:50:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=8A=95=E6=B3=A8?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=BC=B9=E7=AA=97=E4=B8=8E=E6=8A=95=E6=B3=A8?= =?UTF-8?q?=E7=BD=91=E6=A0=BC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新 HallBetResultDialog:根据成功与失败数量显示不同的结果图标与标题。 优化 HallBettingGrid:新增 place trace ID 管理机制,更好地处理投注提交流程,并防止重复扣费。 增强注单详情页面:针对临时状态中的注单自动刷新,提升用户体验。 新增多语言翻译:补充投注结果与状态相关文案,支持更完善的用户反馈。 --- src/features/hall/hall-bet-result-dialog.tsx | 27 ++++++- src/features/hall/hall-betting-grid.tsx | 77 ++++++++++++++----- src/features/hall/hall-wallet-strip.tsx | 7 +- src/features/hall/use-hall-draw-live.ts | 53 ++++++++----- .../orders/ticket-order-detail-screen.tsx | 36 ++++++++- .../orders/ticket-orders-list-screen.tsx | 28 ++++++- src/features/wallet/wallet-transfer-forms.tsx | 24 ++++-- src/hooks/use-websocket-manager.ts | 62 ++++++++------- src/i18n/locales/en/player.json | 11 ++- src/i18n/locales/ne/player.json | 11 ++- src/i18n/locales/zh/player.json | 11 ++- 11 files changed, 257 insertions(+), 90 deletions(-) diff --git a/src/features/hall/hall-bet-result-dialog.tsx b/src/features/hall/hall-bet-result-dialog.tsx index f18cb59..ff80efc 100644 --- a/src/features/hall/hall-bet-result-dialog.tsx +++ b/src/features/hall/hall-bet-result-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { CheckCircle2, ClipboardList, Ticket, XIcon } from "lucide-react"; +import { AlertTriangle, CheckCircle2, ClipboardList, Ticket, XCircle, XIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; @@ -14,6 +14,7 @@ import { } from "@/components/ui/dialog"; import { formatMinorAsCurrency } from "@/lib/money"; import { playLabel } from "@/lib/play-labels"; +import { cn } from "@/lib/utils"; import type { TicketPlaceData } from "@/types/api/ticket"; type HallBetResultDialogProps = { @@ -38,6 +39,15 @@ export function HallBetResultDialog({ const failedItems = data?.items.filter((item) => item.status === "failed") ?? []; const totalSuccess = data?.summary.success_count ?? successItems.length; const totalFailure = data?.summary.failure_count ?? failedItems.length; + const isPartial = totalSuccess > 0 && totalFailure > 0; + const isAllFailed = totalFailure > 0 && totalSuccess === 0; + + const ResultIcon = isAllFailed ? XCircle : isPartial ? AlertTriangle : CheckCircle2; + const iconWrapClass = isAllFailed + ? "border-rose-100 text-rose-600 shadow-[0_10px_24px_rgba(225,29,72,0.12)]" + : isPartial + ? "border-amber-100 text-amber-600 shadow-[0_10px_24px_rgba(217,119,6,0.12)]" + : "border-emerald-100 text-emerald-600 shadow-[0_10px_24px_rgba(22,163,74,0.12)]"; return ( @@ -54,12 +64,21 @@ export function HallBetResultDialog({ > -
- +
+
- {t("hall.result.title")} + {isAllFailed + ? t("hall.result.titleAllFailed") + : isPartial + ? t("hall.result.titlePartial") + : t("hall.result.title")} {data ? ( diff --git a/src/features/hall/hall-betting-grid.tsx b/src/features/hall/hall-betting-grid.tsx index 388ae6d..323a2da 100644 --- a/src/features/hall/hall-betting-grid.tsx +++ b/src/features/hall/hall-betting-grid.tsx @@ -450,6 +450,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } number: null, longPress: false, }); + /** 单次预览→确认共用,重试 place 复用,避免重复扣款 */ + const placeTraceIdRef = useRef(null); + + const newPlaceTraceId = (): string => + typeof crypto !== "undefined" && crypto.randomUUID + ? crypto.randomUUID() + : `pl-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + + const clearPlaceTraceId = () => { + placeTraceIdRef.current = null; + }; const loadCatalog = useCallback(async () => { setCatalogState((s) => (s.kind === "ok" ? s : { kind: "loading" })); try { @@ -466,7 +477,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } const wallet = await getWalletBalance({ currency: currencyParam }); setAvailableMinor(Number(wallet.available_balance ?? 0)); } catch { - setAvailableMinor(0); + // 保留上次可用余额,避免短暂失败导致误报余额不足 } }, [currencyParam]); @@ -892,16 +903,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } return; } + if (previewLoading || placeLoading) { + return; + } + setPreviewLoading(true); try { + placeTraceIdRef.current = newPlaceTraceId(); const data = await postTicketPreview({ draw_id: display.draw_no, currency_code: currencyCode, - client_trace_id: `pv-${ - typeof crypto !== "undefined" && crypto.randomUUID - ? crypto.randomUUID() - : String(Date.now()) - }`, + client_trace_id: placeTraceIdRef.current, lines, }); setPreviewData(data); @@ -925,6 +937,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } const handlePlace = async () => { if (!display || !previewData) return; + if (placeLoading) { + return; + } if (!isBettable) { toast.error(t("hall.closedSubmit")); return; @@ -942,18 +957,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } return; } + const traceId = placeTraceIdRef.current ?? newPlaceTraceId(); + placeTraceIdRef.current = traceId; + setPlaceLoading(true); try { const data = await postTicketPlace({ draw_id: display.draw_no, currency_code: currencyCode, - client_trace_id: - typeof crypto !== "undefined" && crypto.randomUUID - ? crypto.randomUUID() - : `pl-${Date.now()}`, + client_trace_id: traceId, lines, expected_config_versions: previewData.config_versions, }); + clearPlaceTraceId(); setPreviewOpen(false); setPreviewData(null); setResultData(data); @@ -963,17 +979,26 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } triggerWalletPollingAfterBet(); void refreshWallet(); void reloadDraw(); - toast.success( - t("hall.placeSuccess", { - orderNo: data.order_no, - amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode), - }), - ); - if ((data.summary.failure_count ?? 0) > 0) { + const failureCount = data.summary.failure_count ?? 0; + const successCount = data.summary.success_count ?? 0; + if (failureCount > 0 && successCount === 0) { + toast.error( + t("hall.placeAllFailed", { + failed: failureCount, + }), + ); + } else if (failureCount > 0) { toast.warning( t("hall.placePartialFailed", { - success: data.summary.success_count ?? 0, - failed: data.summary.failure_count ?? 0, + success: successCount, + failed: failureCount, + }), + ); + } else { + toast.success( + t("hall.placeSuccess", { + orderNo: data.order_no, + amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode), }), ); } @@ -1268,7 +1293,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } className="w-[4.5rem] min-w-[4.5rem] max-w-[4.5rem] px-0.5 py-2 text-center font-bold" > - {playColumnHeaderLabel(column.play, activeCategory, column.digitSlot, t)} + {playColumnHeaderLabel( + column.play, + activeCategory as Exclude, + column.digitSlot, + t, + )} {t("hall.table.amountPlaceholder")} @@ -1451,7 +1481,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } open={previewOpen} onOpenChange={(open) => { setPreviewOpen(open); - if (!open) setPreviewData(null); + if (!open) { + setPreviewData(null); + if (!placeLoading) { + clearPlaceTraceId(); + } + } }} currencyCode={currencyCode} data={previewData} diff --git a/src/features/hall/hall-wallet-strip.tsx b/src/features/hall/hall-wallet-strip.tsx index 6ab7a5b..cb5d558 100644 --- a/src/features/hall/hall-wallet-strip.tsx +++ b/src/features/hall/hall-wallet-strip.tsx @@ -83,8 +83,7 @@ export function HallWalletStrip() { }; }, [mode, refresh]); - const lotteryMinor = Number(balance?.balance ?? 0); - const availableMinor = Number(balance?.available_balance ?? 0); + const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0); return (
@@ -110,7 +109,7 @@ export function HallWalletStrip() { ) : (

- {formatMinorAsCurrency(lotteryMinor, currency)} + {formatMinorAsCurrency(availableMinor, currency)}

)}
@@ -124,7 +123,7 @@ export function HallWalletStrip() { triggerLabel={t("wallet.transferIn")} triggerClassName="h-12 rounded-lg text-base font-bold" currency={currency} - lotteryMinor={lotteryMinor} + lotteryMinor={availableMinor} onSuccess={refresh} /> s.setWalletPollingExpiryAt, ); + const latestSnapshotMsRef = useRef(0); + + const applySnapshot = useCallback((anchorMs: number, data: DrawCurrentPayload | null) => { + if (anchorMs < latestSnapshotMsRef.current) { + return; + } + latestSnapshotMsRef.current = anchorMs; + setServerNowMs(anchorMs); + setRaw(data); + setEmittedAtMs(anchorMs); + }, []); + const mergeFromWs = useCallback((evt: HallWsEnvelope) => { const anchor = evt.emitted_at_ms ?? Date.now(); - setServerNowMs(anchor); - setRaw(evt.data); - setEmittedAtMs(anchor); - }, []); + applySnapshot(anchor, evt.data); + }, [applySnapshot]); const mergeCountdownFromWs = useCallback((evt: HallWsEnvelope) => { if (evt.data === null) return; const anchor = evt.emitted_at_ms ?? Date.now(); - setServerNowMs(anchor); - setRaw(evt.data); - setEmittedAtMs(anchor); - }, []); + applySnapshot(anchor, evt.data); + }, [applySnapshot]); - const updateFromResponse = useCallback((resp: DrawCurrentResponse) => { - setServerNowMs(resp.server_now_ms); - setRaw(resp.data); - setEmittedAtMs(resp.server_now_ms); - }, []); - - const load = useCallback(async () => { + const load = useCallback(async (options?: { force?: boolean }) => { try { setError(null); const d = await getDrawCurrent(); - updateFromResponse(d); + const wsConnected = useNetworkConnectionStore.getState().isWebSocketConnected; + if (!options?.force && wsConnected && d.server_now_ms < latestSnapshotMsRef.current) { + return; + } + applySnapshot(d.server_now_ms, d.data); } catch { setError("draw.loadFailedRefresh"); setRaw(undefined); } - }, [updateFromResponse]); + }, [applySnapshot]); // 初始加载 useEffect(() => { @@ -212,7 +218,9 @@ export function useHallDrawLive(): HallDrawLiveSnapshot { } return () => { - echo.leave("lottery-hall"); + channel.stopListening(".draw.countdown"); + channel.stopListening(".draw.status_change"); + channel.stopListening(".result.published"); if (echo.connector?.pusher) { echo.connector.pusher.connection.unbind("connected", handleConnected); echo.connector.pusher.connection.unbind( @@ -312,7 +320,7 @@ export function useHallDrawLive(): HallDrawLiveSnapshot { if (trigger && zeroRefreshKeyRef.current !== trigger) { zeroRefreshKeyRef.current = trigger; - void load(); + void load({ force: true }); } }, [display, nowMs, load]); @@ -342,14 +350,17 @@ export function useHallDrawLive(): HallDrawLiveSnapshot { return () => window.clearInterval(intervalId); }, [needsFastDrawPoll, load]); - // WebSocket 已连接时的兜底轮询(tick 延迟时的保险) + // WebSocket 已连接时的兜底轮询(tick 延迟时的保险;常态下由 WS 推送,避免 HTTP 覆盖新快照) useEffect(() => { + if (isWebSocketConnected && !needsFastDrawPoll) { + return; + } const intervalMs = needsFastDrawPoll ? 15_000 : 45_000; const intervalId = window.setInterval(() => { void load(); }, intervalMs); return () => window.clearInterval(intervalId); - }, [load, needsFastDrawPoll]); + }, [load, needsFastDrawPoll, isWebSocketConnected]); return { raw, display, serverNowMs, nowMs, error, reload: load, isBettable }; } diff --git a/src/features/orders/ticket-order-detail-screen.tsx b/src/features/orders/ticket-order-detail-screen.tsx index c10c737..dfaf95b 100644 --- a/src/features/orders/ticket-order-detail-screen.tsx +++ b/src/features/orders/ticket-order-detail-screen.tsx @@ -52,6 +52,13 @@ type TimelineRow = { time: string; }; +const TRANSIENT_TICKET_STATUSES = new Set([ + "pending_confirm", + "partial_pending_confirm", + "pending_draw", + "pending_payout", +]); + type TicketItemDetailWithExtras = TicketItemDetailPayload & { timeline?: TimelineRow[]; match_result?: { @@ -98,17 +105,23 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) { }; }, [fromGroupKey, groupTickets, t]); - const load = useCallback(async () => { - setLoading(true); + const load = useCallback(async (options?: { silent?: boolean }) => { + if (!options?.silent) { + setLoading(true); + } setError(null); try { const row = await getTicketItemDetail(ticketNo); setData(row); } catch { setData(null); - setError(t("orders.notFound")); + if (!options?.silent) { + setError(t("orders.notFound")); + } } finally { - setLoading(false); + if (!options?.silent) { + setLoading(false); + } } }, [ticketNo, t]); @@ -118,6 +131,21 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) { }); }, [load]); + useEffect(() => { + if (!data || !TRANSIENT_TICKET_STATUSES.has(data.status)) { + return; + } + const refresh = () => void load({ silent: true }); + const intervalId = window.setInterval(refresh, 12_000); + window.addEventListener("lottery-hall-refresh", refresh); + window.addEventListener("lottery-wallet-refresh", refresh); + return () => { + window.clearInterval(intervalId); + window.removeEventListener("lottery-hall-refresh", refresh); + window.removeEventListener("lottery-wallet-refresh", refresh); + }; + }, [data?.status, load]); + if (loading) { return ( { + const refreshFromEvents = () => { + void fetchPage(1, false); + }; + window.addEventListener("lottery-hall-refresh", refreshFromEvents); + window.addEventListener("lottery-wallet-refresh", refreshFromEvents); + return () => { + window.removeEventListener("lottery-hall-refresh", refreshFromEvents); + window.removeEventListener("lottery-wallet-refresh", refreshFromEvents); + }; + }, [fetchPage]); + useEffect(() => { const target = loadMoreRef.current; if (!target || loading || loadingMore || page >= lastPage) return; @@ -238,6 +261,9 @@ export function TicketOrdersListScreen() { } /> +

+ {t("orders.dateRangeHint")} +

(null); + const idempotentKeyRef = useRef(null); const tid = `${idPrefix}in-amount`; const parsedMinor = useMemo( @@ -159,25 +160,31 @@ export function TransferInPanel({ parsedMinor != null ? lotteryMinor + parsedMinor : lotteryMinor; const submit = async () => { + if (submitting) { + return; + } setLocalError(null); if (parsedMinor == null || parsedMinor < 1) { setLocalError(t("wallet.invalidAmount")); return; } + if (idempotentKeyRef.current === null) { + idempotentKeyRef.current = randomIdempotentKey(); + } setSubmitting(true); try { await postWalletTransferIn({ amount: parsedMinor, currency, - idempotent_key: randomIdempotentKey(), + idempotent_key: idempotentKeyRef.current, }); + idempotentKeyRef.current = null; toast.success(t("wallet.successIn")); setAmountText(""); await onSuccess(); emitWalletRefresh(); } catch (e) { if (await handleTransferMaybePending(e, onSuccess, t)) { - setLocalError(formatWalletClientError(e, t)); return; } setLocalError(formatWalletClientError(e, t)); @@ -270,6 +277,7 @@ export function TransferOutPanel({ const [amountText, setAmountText] = useState(""); const [submitting, setSubmitting] = useState(false); const [localError, setLocalError] = useState(null); + const idempotentKeyRef = useRef(null); const tid = `${idPrefix}out-amount`; const parsedMinor = useMemo( @@ -288,6 +296,9 @@ export function TransferOutPanel({ }; const submit = async () => { + if (submitting) { + return; + } setLocalError(null); if (parsedMinor == null || parsedMinor < 1) { setLocalError(t("wallet.invalidAmount")); @@ -297,20 +308,23 @@ export function TransferOutPanel({ setLocalError(t("wallet.outExceeds")); return; } + if (idempotentKeyRef.current === null) { + idempotentKeyRef.current = randomIdempotentKey(); + } setSubmitting(true); try { await postWalletTransferOut({ amount: parsedMinor, currency, - idempotent_key: randomIdempotentKey(), + idempotent_key: idempotentKeyRef.current, }); + idempotentKeyRef.current = null; toast.success(t("wallet.successOut")); setAmountText(""); await onSuccess(); emitWalletRefresh(); } catch (e) { if (await handleTransferMaybePending(e, onSuccess, t)) { - setLocalError(formatWalletClientError(e, t)); return; } setLocalError(formatWalletClientError(e, t)); diff --git a/src/hooks/use-websocket-manager.ts b/src/hooks/use-websocket-manager.ts index 0f37dcc..0ff41b5 100644 --- a/src/hooks/use-websocket-manager.ts +++ b/src/hooks/use-websocket-manager.ts @@ -259,46 +259,55 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { }; }, [mode, reconnect, store]); - // 初始化和 WebSocket 监控 + // 初始化:绑定 Pusher 真实连接事件(勿仅凭 channel 对象存在判定已连接) useEffect(() => { const echo = getLotteryEcho(); if (!echo) { - // 没有 Echo 配置,直接启用轮询模式 switchToPollingMode(); startDrawPolling(); return; } - // 尝试初始连接 - const success = connectWebSocket(); - if (success) { - handleConnect(); - } else { - handleDisconnect(); - } + echo.channel("lottery-hall"); - // 设置连接状态监控(通过定期订阅状态) - const checkConnection = () => { - try { - // 尝试访问频道来测试连接 - const channel = echo.channel("lottery-hall"); - if (channel) { - if (!isWebSocketConnected) { - handleConnect(); - } - } - } catch { - if (isWebSocketConnected) { - handleDisconnect(); - } + const pusher = echo.connector?.pusher; + const connection = pusher?.connection; + + const syncConnectionState = () => { + const state = connection?.state; + if (state === "connected") { + handleConnect(); + } else if ( + state === "disconnected" || + state === "unavailable" || + state === "failed" + ) { + handleDisconnect(); } }; - // 每 5 秒检查一次连接状态 - const checkInterval = window.setInterval(checkConnection, 5000); + if (connection) { + connection.bind("connected", handleConnect); + connection.bind("disconnected", handleDisconnect); + connection.bind("unavailable", handleDisconnect); + connection.bind("failed", handleDisconnect); + syncConnectionState(); + } else { + const success = connectWebSocket(); + if (success) { + handleConnect(); + } else { + handleDisconnect(); + } + } return () => { - window.clearInterval(checkInterval); + if (connection) { + connection.unbind("connected", handleConnect); + connection.unbind("disconnected", handleDisconnect); + connection.unbind("unavailable", handleDisconnect); + connection.unbind("failed", handleDisconnect); + } if (reconnectTimerRef.current) { window.clearTimeout(reconnectTimerRef.current); } @@ -307,7 +316,6 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { connectWebSocket, handleConnect, handleDisconnect, - isWebSocketConnected, startDrawPolling, switchToPollingMode, ]); diff --git a/src/i18n/locales/en/player.json b/src/i18n/locales/en/player.json index c23f434..28a6d30 100644 --- a/src/i18n/locales/en/player.json +++ b/src/i18n/locales/en/player.json @@ -120,6 +120,7 @@ "placeFailed": "Submission failed", "placeSuccess": "Bet submitted. Order {{orderNo}}, deducted {{amount}}.", "placePartialFailed": "{{success}} succeeded, {{failed}} failed", + "placeAllFailed": "All {{failed}} lines failed", "playConfig": { "playClosedDraftCleared": "{{playCode}} is closed. Related draft amounts have been cleared.", "playClosed": "{{playCode}} is closed.", @@ -244,7 +245,9 @@ "warningsDescription": "The following numbers have high payout pool usage for this issue. Betting is still allowed, but the order may be rejected as sold out if capacity is insufficient." }, "result": { - "title": "Bet result", + "title": "Bet placed", + "titlePartial": "Partially placed", + "titleAllFailed": "Bet not placed", "draw": "Issue", "empty": "No result yet.", "successCount": "Successful lines", @@ -401,6 +404,7 @@ "betNow": "Bet Now", "empty": "No bet records yet.", "dateRange": "Date range", + "dateRangeHint": "Filters by order time in schedule day (UTC)", "status": "Status", "statusFilter": "Status filter", "noMore": "No more tickets", @@ -575,11 +579,16 @@ }, "ticketStatus": { "success": "Awaiting draw", + "placed": "Awaiting draw", + "pending_confirm": "Confirming", + "partial_pending_confirm": "Partially confirming", "pending_draw": "Awaiting draw", + "partial_failed": "Partially failed", "pending_payout": "Won, pending payout", "settled_win": "Paid", "settled_lose": "Not won", "failed": "Failed", + "refunded": "Refunded", "unknown": "{{status}}" }, "prizeTier": { diff --git a/src/i18n/locales/ne/player.json b/src/i18n/locales/ne/player.json index 9c51dbf..477ea7d 100644 --- a/src/i18n/locales/ne/player.json +++ b/src/i18n/locales/ne/player.json @@ -120,6 +120,7 @@ "placeFailed": "पेश गर्न असफल", "placeSuccess": "बेट पेश भयो। अर्डर {{orderNo}}, कट्टा {{amount}}।", "placePartialFailed": "{{success}} सफल, {{failed}} असफल", + "placeAllFailed": "सबै {{failed}} लाइन असफल", "playConfig": { "playClosedDraftCleared": "{{playCode}} बन्द छ। सम्बन्धित ड्राफ्ट रकम हटाइएको छ।", "playClosed": "{{playCode}} बन्द छ।", @@ -244,7 +245,9 @@ "warningsDescription": "यी नम्बरहरूमा यस इश्यूमा भुक्तानी पूल प्रयोग उच्च छ। बेट अझै गर्न सकिन्छ, तर क्षमता अपुग भए अर्डर sold out हुन सक्छ।" }, "result": { - "title": "बेट नतिजा", + "title": "बेट सफल", + "titlePartial": "आंशिक सफल", + "titleAllFailed": "बेट असफल", "draw": "इश्यू", "empty": "नतिजा छैन।", "successCount": "सफल लाइनहरू", @@ -401,6 +404,7 @@ "betNow": "अहिले बेट", "empty": "अहिलेसम्म बेट रेकर्ड छैन।", "dateRange": "मिति दायरा", + "dateRangeHint": "अर्डर समयलाई तालिका दिन (UTC) अनुसार फिल्टर गर्छ", "status": "स्थिति", "statusFilter": "स्थिति फिल्टर", "noMore": "थप टिकट छैन", @@ -575,11 +579,16 @@ }, "ticketStatus": { "success": "ड्र पर्खँदै", + "placed": "ड्र पर्खँदै", + "pending_confirm": "पुष्टि हुँदै", + "partial_pending_confirm": "आंशिक पुष्टि", "pending_draw": "ड्र पर्खँदै", + "partial_failed": "आंशिक असफल", "pending_payout": "जितेको, भुक्तानी बाँकी", "settled_win": "भुक्तानी भयो", "settled_lose": "जितेन", "failed": "असफल", + "refunded": "फिर्ता", "unknown": "{{status}}" }, "prizeTier": { diff --git a/src/i18n/locales/zh/player.json b/src/i18n/locales/zh/player.json index 9253529..dfad47a 100644 --- a/src/i18n/locales/zh/player.json +++ b/src/i18n/locales/zh/player.json @@ -243,8 +243,11 @@ "warningsTitle": "赔付池预警", "warningsDescription": "以下号码本期赔付池占用较高,仍允许下注;若实际占用不足将售罄拒单。" }, + "placeAllFailed": "本次 {{failed}} 条注项均未成功", "result": { - "title": "下注结果", + "title": "下注成功", + "titlePartial": "部分注项成功", + "titleAllFailed": "下注未成功", "draw": "期号", "empty": "暂无结果。", "successCount": "成功注项", @@ -401,6 +404,7 @@ "betNow": "立即下注", "empty": "暂无下注记录。", "dateRange": "日期范围", + "dateRangeHint": "按彩票排期日(UTC)筛选下单时间", "status": "状态", "statusFilter": "状态筛选", "noMore": "没有更多注单", @@ -575,11 +579,16 @@ }, "ticketStatus": { "success": "待开奖", + "placed": "待开奖", + "pending_confirm": "确认中", + "partial_pending_confirm": "部分确认中", "pending_draw": "待开奖", + "partial_failed": "部分失败", "pending_payout": "已中奖待派彩", "settled_win": "已派彩", "settled_lose": "未中奖", "failed": "失败", + "refunded": "已退款", "unknown": "{{status}}" }, "prizeTier": {