diff --git a/src/features/hall/hall-bet-errors.ts b/src/features/hall/hall-bet-errors.ts
index 5b65af7..806152e 100644
--- a/src/features/hall/hall-bet-errors.ts
+++ b/src/features/hall/hall-bet-errors.ts
@@ -36,6 +36,13 @@ export function mapTicketBetError(
"hall.ticketError.2008",
"赔率或玩法配置已更新,请关闭预览后重新操作。",
);
+ case 2009:
+ return msg(
+ "hall.ticketError.2009",
+ "该订单已退款或不可重复提交,请关闭预览后重新下注。",
+ );
+ case 1007:
+ return msg("hall.ticketError.1007", "彩票钱包已冻结,暂无法下注。");
case 1003:
return msg("hall.ticketError.1003", "下注金额超出该玩法允许范围。");
default:
diff --git a/src/features/hall/hall-bet-result-dialog.tsx b/src/features/hall/hall-bet-result-dialog.tsx
index ff80efc..d7ad22a 100644
--- a/src/features/hall/hall-bet-result-dialog.tsx
+++ b/src/features/hall/hall-bet-result-dialog.tsx
@@ -25,6 +25,9 @@ type HallBetResultDialogProps = {
jackpotEnabled?: boolean;
};
+const SUCCESS_ITEM_STATUSES = new Set(["pending_draw", "placed"]);
+const FAILURE_ITEM_STATUSES = new Set(["failed", "refunded"]);
+
export function HallBetResultDialog({
open,
onOpenChange,
@@ -34,18 +37,41 @@ export function HallBetResultDialog({
}: HallBetResultDialogProps) {
const { t } = useTranslation("player");
- // 成功注项状态为 pending_draw(待开奖),不是 success
- const successItems = data?.items.filter((item) => item.status !== "failed") ?? [];
- const failedItems = data?.items.filter((item) => item.status === "failed") ?? [];
+ const successItems =
+ data?.items.filter((item) => SUCCESS_ITEM_STATUSES.has(item.status)) ?? [];
+ const failedItems =
+ data?.items.filter((item) => FAILURE_ITEM_STATUSES.has(item.status)) ?? [];
+ const pendingItems =
+ data?.items.filter((item) => item.status === "pending_confirm") ?? [];
+
+ const orderStatus = data?.summary.order_status;
+ const isRefundedOrder = orderStatus === "refunded";
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 pendingConfirmCount =
+ data?.summary.pending_confirm_count ?? pendingItems.length;
- const ResultIcon = isAllFailed ? XCircle : isPartial ? AlertTriangle : CheckCircle2;
+ const isAllFailed =
+ isRefundedOrder ||
+ (totalFailure > 0 && totalSuccess === 0 && pendingConfirmCount === 0);
+ const isPartial =
+ !isRefundedOrder &&
+ totalSuccess > 0 &&
+ (totalFailure > 0 || pendingConfirmCount > 0);
+ const isPendingOnly =
+ !isRefundedOrder &&
+ pendingConfirmCount > 0 &&
+ totalSuccess === 0 &&
+ totalFailure === 0;
+
+ const ResultIcon = isAllFailed
+ ? XCircle
+ : isPartial || isPendingOnly
+ ? AlertTriangle
+ : CheckCircle2;
const iconWrapClass = isAllFailed
? "border-rose-100 text-rose-600 shadow-[0_10px_24px_rgba(225,29,72,0.12)]"
- : isPartial
+ : isPartial || isPendingOnly
? "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)]";
@@ -74,11 +100,15 @@ export function HallBetResultDialog({
- {isAllFailed
- ? t("hall.result.titleAllFailed")
- : isPartial
- ? t("hall.result.titlePartial")
- : t("hall.result.title")}
+ {isRefundedOrder
+ ? t("hall.result.titleRefunded")
+ : isAllFailed
+ ? t("hall.result.titleAllFailed")
+ : isPartial
+ ? t("hall.result.titlePartial")
+ : isPendingOnly
+ ? t("hall.result.titlePendingConfirm")
+ : t("hall.result.title")}
{data ? (
@@ -99,6 +129,16 @@ export function HallBetResultDialog({
) : (
<>
+ {isRefundedOrder ? (
+
+ {isPartialFailedOrder ? (
+
+
{t("orders.partialFailedOrderTitle")}
+
+ {t("orders.partialFailedOrderBody")}
+
+
+ ) : null}
+ {isLineFailed && failReason ? (
+
+
{t("orders.lineFailedTitle")}
+
{failReason}
+
+ ) : null}
{t("orders.drawNo")}
diff --git a/src/features/orders/ticket-order-group-screen.tsx b/src/features/orders/ticket-order-group-screen.tsx
index 687886e..8871461 100644
--- a/src/features/orders/ticket-order-group-screen.tsx
+++ b/src/features/orders/ticket-order-group-screen.tsx
@@ -95,6 +95,11 @@ export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps
+ {group.status === "partial_failed" ? (
+
+ {t("orders.partialFailedHint")}
+
+ ) : null}
{totalWin > 0 && group.status === "settled_win" ? (
{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
@@ -147,6 +152,11 @@ export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps
+ {row.status === "failed" || row.status === "refunded" ? (
+
+ {t("orders.lineFailedTitle")}
+
+ ) : null}
{lineWin > 0 && row.status === "settled_win" ? (
{t("orders.win", { amount: formatMinorAsCurrency(lineWin, lineCur) })}
diff --git a/src/features/orders/ticket-orders-list-screen.tsx b/src/features/orders/ticket-orders-list-screen.tsx
index aa7dbdc..a1e044b 100644
--- a/src/features/orders/ticket-orders-list-screen.tsx
+++ b/src/features/orders/ticket-orders-list-screen.tsx
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CalendarRange, ChevronDown, Search } from "lucide-react";
import { useTranslation } from "react-i18next";
+import { getDrawCurrent } from "@/api/draw";
import { getTicketItems } from "@/api/ticket-items";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@@ -23,7 +24,14 @@ import { OrderMetaLine } from "@/features/orders/order-meta-line";
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
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 {
+ formatSchedulePickerYmd,
+ getTimeZoneShortLabel,
+ parseSchedulePickerYmd,
+ scheduleTodayYmd,
+} from "@/lib/player-datetime";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { playLabel } from "@/lib/play-labels";
import { cn } from "@/lib/utils";
@@ -43,20 +51,6 @@ const STATUS_OPTIONS = [
"refunded",
] as const;
-function parseYmd(value: string): Date | undefined {
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
- if (!m) return undefined;
- const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
- return Number.isNaN(d.getTime()) ? undefined : d;
-}
-
-function formatYmd(value: Date): string {
- const y = value.getFullYear();
- const m = String(value.getMonth() + 1).padStart(2, "0");
- const d = String(value.getDate()).padStart(2, "0");
- return `${y}-${m}-${d}`;
-}
-
export function TicketOrdersListScreen() {
const searchParams = useSearchParams();
const { t } = useTranslation("player");
@@ -83,13 +77,18 @@ export function TicketOrdersListScreen() {
const [rangeOpen, setRangeOpen] = useState(false);
const [statusOpen, setStatusOpen] = useState(false);
const [calendarMonth, setCalendarMonth] = useState(() => new Date());
+ const [scheduleTimezone, setScheduleTimezone] = useState(LOTTERY_SCHEDULE_TIMEZONE);
const loadMoreRef = useRef(null);
const isMobile = useIsMobile();
const initialLoadDone = useRef(false);
+ useEffect(() => {
+ setQueryDrawNo(drawNoFilter);
+ }, [drawNoFilter]);
+
const selectedRange = useMemo(() => {
- const from = parseYmd(fromDate);
- const to = parseYmd(toDate);
+ const from = parseSchedulePickerYmd(fromDate);
+ const to = parseSchedulePickerYmd(toDate);
if (!from && !to) return undefined;
if (from && to) return { from, to };
if (from) return { from };
@@ -140,6 +139,22 @@ export function TicketOrdersListScreen() {
[drawNoFilter, fromDate, queryDrawNo, queryNumber, queryStatuses, t, toDate],
);
+ useEffect(() => {
+ void getDrawCurrent()
+ .then((res) => {
+ const tz = res.data?.schedule_timezone?.trim();
+ if (tz) setScheduleTimezone(tz);
+ })
+ .catch(() => {
+ /* 保留默认排期时区 */
+ });
+ }, []);
+
+ const scheduleTzLabel = useMemo(
+ () => getTimeZoneShortLabel(scheduleTimezone),
+ [scheduleTimezone],
+ );
+
useEffect(() => {
if (!initialLoadDone.current) {
initialLoadDone.current = true;
@@ -262,7 +277,7 @@ export function TicketOrdersListScreen() {
/>
- {t("orders.dateRangeHint")}
+ {t("orders.dateRangeHint", { tz: scheduleTzLabel })}
-
+
+
+
+
@@ -437,6 +466,11 @@ export function TicketOrdersListScreen() {
+ {group.status === "partial_failed" ? (
+
+ {t("orders.partialFailedHint")}
+
+ ) : null}
{totalWin > 0 && group.status === "settled_win" ? (
{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
@@ -498,11 +532,11 @@ export function TicketOrdersListScreen() {
{t("actions.next")}
- ) : (
+ ) : lastPage > 1 ? (
{t("orders.noMore")}
- )}
+ ) : null}
>
)}
diff --git a/src/features/wallet/wallet-logs-screen.tsx b/src/features/wallet/wallet-logs-screen.tsx
index 99f6d89..441954d 100644
--- a/src/features/wallet/wallet-logs-screen.tsx
+++ b/src/features/wallet/wallet-logs-screen.tsx
@@ -25,20 +25,6 @@ export function WalletLogsScreen() {
const [error, setError] = useState(null);
const loadMoreRef = useRef(null);
- const logsForCurrency = useMemo(() => {
- if (!logs) return null;
- const code = currency.toUpperCase();
- return {
- ...logs,
- items: logs.items.filter(
- (item) => (item.currency_code || code).toUpperCase() === code,
- ),
- pending_reconcile: logs.pending_reconcile.filter(
- (item) => item.currency_code.toUpperCase() === code,
- ),
- };
- }, [currency, logs]);
-
const fetchPassRef = useRef(true);
const load = useCallback(async (targetPage = 1, append = false) => {
@@ -56,6 +42,7 @@ export function WalletLogsScreen() {
page: targetPage,
size: WALLET_LOGS_PAGE_SIZE,
type: filter || undefined,
+ currency,
});
setLogs((current) =>
append && current
@@ -72,7 +59,7 @@ export function WalletLogsScreen() {
setLogsLoading(false);
setLoadingMore(false);
}
- }, [filter, t]);
+ }, [currency, filter, t]);
useEffect(() => {
queueMicrotask(() => {
@@ -131,7 +118,7 @@ export function WalletLogsScreen() {
) : null}
append && current
@@ -49,7 +50,7 @@ export function WalletScreen() {
: nextLogs,
);
return nextLogs;
- }, [filter]);
+ }, [currency, filter]);
useEffect(() => {
let cancelled = false;
@@ -107,20 +108,6 @@ export function WalletScreen() {
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
}, [refreshAll]);
- const logsForCurrency = useMemo(() => {
- if (!logs) return null;
- const code = currency.toUpperCase();
- return {
- ...logs,
- items: logs.items.filter(
- (item) => (item.currency_code || code).toUpperCase() === code,
- ),
- pending_reconcile: logs.pending_reconcile.filter(
- (item) => item.currency_code.toUpperCase() === code,
- ),
- };
- }, [currency, logs]);
-
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
const loadMore = useCallback(() => {
@@ -222,7 +209,7 @@ export function WalletScreen() {
{
- // 检查是否过期
- if (store.walletPollingExpiryAt && Date.now() > store.walletPollingExpiryAt) {
+ const live = useNetworkConnectionStore.getState();
+ if (live.walletPollingExpiryAt && Date.now() > live.walletPollingExpiryAt) {
window.clearInterval(intervalId);
- store.setWalletPollingIntervalId(null);
+ live.setWalletPollingIntervalId(null);
return;
}
void getWalletBalance({ currency: getActivePlayerCurrencyFromStore() }).then(() => {
@@ -167,8 +167,8 @@ export function triggerWalletPollingAfterBet(): void {
});
}, POLLING_INTERVAL_MS);
- store.setWalletPollingIntervalId(intervalId);
- store.setWalletPollingExpiryAt(Date.now() + LIMITED_POLLING_DURATION_MS);
+ useNetworkConnectionStore.getState().setWalletPollingIntervalId(intervalId);
+ useNetworkConnectionStore.getState().setWalletPollingExpiryAt(Date.now() + LIMITED_POLLING_DURATION_MS);
// 2分钟后自动清理
window.setTimeout(() => {
diff --git a/src/hooks/use-websocket-manager.ts b/src/hooks/use-websocket-manager.ts
index 0ff41b5..503e758 100644
--- a/src/hooks/use-websocket-manager.ts
+++ b/src/hooks/use-websocket-manager.ts
@@ -2,7 +2,6 @@
import { useCallback, useEffect, useRef } from "react";
-import { getDrawCurrent } from "@/api/draw";
import { getWalletBalance } from "@/api/wallet";
import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency";
import { getLotteryEcho } from "@/lib/lottery-echo";
@@ -63,11 +62,8 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
// 刷新画作数据
const refreshDraw = useCallback(async () => {
- try {
- await getDrawCurrent();
- } catch {
- // 静默处理错误,避免频繁报错
- }
+ // 由 useHallDrawLive 监听并拉取,避免与大厅重复请求 draw/current
+ window.dispatchEvent(new Event("lottery-hall-refresh"));
}, []);
// 刷新钱包余额
@@ -138,21 +134,11 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
clearWalletPolling();
}, [clearWalletPolling]);
- // 尝试连接 WebSocket
- const connectWebSocket = useCallback(() => {
+ // 检测 Pusher 是否已真正 connected(非仅订阅频道)
+ const isPusherConnected = useCallback((): boolean => {
const echo = getLotteryEcho();
- if (!echo) {
- // 配置不完整,无法连接
- return false;
- }
-
- try {
- // 连接到 lottery-hall 频道以测试连接
- echo.channel("lottery-hall");
- return true;
- } catch {
- return false;
- }
+ const state = echo?.connector?.pusher?.connection?.state;
+ return state === "connected";
}, []);
// 处理连接断开
@@ -207,20 +193,19 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
incrementReconnectAttempts();
- const success = connectWebSocket();
- if (success) {
+ if (isPusherConnected()) {
handleConnect();
- } else {
- // 继续重连
- reconnectTimerRef.current = window.setTimeout(
- () => attemptReconnectRef.current(),
- RECONNECT_INTERVAL_MS,
- );
+ return;
}
+
+ reconnectTimerRef.current = window.setTimeout(
+ () => attemptReconnectRef.current(),
+ RECONNECT_INTERVAL_MS,
+ );
}, [
reconnectAttempts,
incrementReconnectAttempts,
- connectWebSocket,
+ isPusherConnected,
handleConnect,
setReconnecting,
]);
@@ -292,13 +277,10 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
connection.bind("unavailable", handleDisconnect);
connection.bind("failed", handleDisconnect);
syncConnectionState();
+ } else if (isPusherConnected()) {
+ handleConnect();
} else {
- const success = connectWebSocket();
- if (success) {
- handleConnect();
- } else {
- handleDisconnect();
- }
+ handleDisconnect();
}
return () => {
@@ -313,7 +295,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
}
};
}, [
- connectWebSocket,
+ isPusherConnected,
handleConnect,
handleDisconnect,
startDrawPolling,
diff --git a/src/i18n/locales/en/player.json b/src/i18n/locales/en/player.json
index 28a6d30..5deab19 100644
--- a/src/i18n/locales/en/player.json
+++ b/src/i18n/locales/en/player.json
@@ -248,6 +248,11 @@
"title": "Bet placed",
"titlePartial": "Partially placed",
"titleAllFailed": "Bet not placed",
+ "titleRefunded": "Order refunded",
+ "titlePendingConfirm": "Pending confirmation",
+ "refundedHint": "This order was already refunded. Close and preview again to place a new bet.",
+ "pendingConfirmHint": "Your bet was submitted. Wallet deduction is being confirmed — check My Bets shortly.",
+ "pendingConfirmLines": "{{count}} line(s) pending wallet confirmation",
"draw": "Issue",
"empty": "No result yet.",
"successCount": "Successful lines",
@@ -306,6 +311,7 @@
"2006": "This issue cannot accept bets.",
"2007": "This play type is unsupported or missing odds configuration.",
"2008": "Odds or play configuration has changed. Close the preview and try again.",
+ "2009": "This order was refunded or cannot be resubmitted. Close the preview and place a new bet.",
"1003": "Stake amount is outside the allowed range for this play type.",
"fallback": "Bet failed. Please try again later."
},
@@ -404,7 +410,12 @@
"betNow": "Bet Now",
"empty": "No bet records yet.",
"dateRange": "Date range",
- "dateRangeHint": "Filters by order time in schedule day (UTC)",
+ "dateRangeHint": "Filters order time by schedule day ({{tz}})",
+ "scheduleToday": "Schedule today",
+ "partialFailedHint": "Some lines in this order did not succeed — check each line",
+ "partialFailedOrderTitle": "Partially failed order",
+ "partialFailedOrderBody": "Some lines in this order succeeded and some did not. This page shows this line only.",
+ "lineFailedTitle": "This line did not succeed",
"status": "Status",
"statusFilter": "Status filter",
"noMore": "No more tickets",
diff --git a/src/i18n/locales/ne/player.json b/src/i18n/locales/ne/player.json
index 477ea7d..1d902dc 100644
--- a/src/i18n/locales/ne/player.json
+++ b/src/i18n/locales/ne/player.json
@@ -248,6 +248,11 @@
"title": "बेट सफल",
"titlePartial": "आंशिक सफल",
"titleAllFailed": "बेट असफल",
+ "titleRefunded": "अर्डर फिर्ता",
+ "titlePendingConfirm": "पुष्टि बाँकी",
+ "refundedHint": "यो अर्डर पहिले नै फिर्ता भइसकेको छ। नयाँ बेटका लागि पूर्वावलोकन बन्द गरी फेरि गर्नुहोस्।",
+ "pendingConfirmHint": "बेट पेश भयो। वालेट कट्टी पुष्टि हुँदैछ — छिट्टै मेरो बेट हेर्नुहोस्।",
+ "pendingConfirmLines": "{{count}} लाइन कट्टी पुष्टि बाँकी",
"draw": "इश्यू",
"empty": "नतिजा छैन।",
"successCount": "सफल लाइनहरू",
@@ -306,6 +311,7 @@
"2006": "यो इश्यूले बेट स्वीकार गर्न सक्दैन।",
"2007": "यो प्ले प्रकार समर्थित छैन वा odds कन्फिगरेसन छैन।",
"2008": "Odds वा प्ले कन्फिगरेसन परिवर्तन भयो। पूर्वावलोकन बन्द गरी फेरि प्रयास गर्नुहोस्।",
+ "2009": "यो अर्डर फिर्ता भइसकेको छ वा पुन: पेश गर्न मिल्दैन। पूर्वावलोकन बन्द गरी नयाँ बेट गर्नुहोस्।",
"1003": "बेट रकम यो प्ले प्रकारको अनुमत दायराभन्दा बाहिर छ।",
"fallback": "बेट असफल। कृपया पछि प्रयास गर्नुहोस्।"
},
@@ -404,7 +410,12 @@
"betNow": "अहिले बेट",
"empty": "अहिलेसम्म बेट रेकर्ड छैन।",
"dateRange": "मिति दायरा",
- "dateRangeHint": "अर्डर समयलाई तालिका दिन (UTC) अनुसार फिल्टर गर्छ",
+ "dateRangeHint": "अर्डर समय तालिका दिन ({{tz}}) अनुसार फिल्टर",
+ "scheduleToday": "तालिका आज",
+ "partialFailedHint": "यो अर्डरका केही लाइन सफल भएनन् — प्रत्येक लाइन हेर्नुहोस्",
+ "partialFailedOrderTitle": "आंशिक असफल अर्डर",
+ "partialFailedOrderBody": "यस अर्डरका केही लाइन सफल, केही असफल। यो पृष्ठमा यो लाइन मात्र देखाइएको छ।",
+ "lineFailedTitle": "यो लाइन सफल भएन",
"status": "स्थिति",
"statusFilter": "स्थिति फिल्टर",
"noMore": "थप टिकट छैन",
diff --git a/src/i18n/locales/zh/player.json b/src/i18n/locales/zh/player.json
index dfad47a..d888bbb 100644
--- a/src/i18n/locales/zh/player.json
+++ b/src/i18n/locales/zh/player.json
@@ -248,6 +248,11 @@
"title": "下注成功",
"titlePartial": "部分注项成功",
"titleAllFailed": "下注未成功",
+ "titleRefunded": "订单已退款",
+ "titlePendingConfirm": "待确认扣款",
+ "refundedHint": "该订单此前已退款,未产生新的扣款。请关闭后重新预览并下注。",
+ "pendingConfirmHint": "注项已提交,钱包扣款确认中,请稍后在「我的注单」查看最终状态。",
+ "pendingConfirmLines": "{{count}} 笔注项待确认扣款",
"draw": "期号",
"empty": "暂无结果。",
"successCount": "成功注项",
@@ -306,6 +311,8 @@
"2006": "当前期号不可下注。",
"2007": "该玩法暂不支持或缺少赔率配置。",
"2008": "赔率或玩法配置已更新,请关闭预览后重新操作。",
+ "2009": "该订单已退款或不可重复提交,请关闭预览后重新下注。",
+ "1007": "彩票钱包已冻结,暂无法下注。",
"1003": "下注金额超出该玩法允许范围。",
"fallback": "下注失败,请稍后重试。"
},
@@ -404,7 +411,12 @@
"betNow": "立即下注",
"empty": "暂无下注记录。",
"dateRange": "日期范围",
- "dateRangeHint": "按彩票排期日(UTC)筛选下单时间",
+ "dateRangeHint": "按彩票排期日({{tz}})筛选下单时间",
+ "scheduleToday": "排期今天",
+ "partialFailedHint": "本单部分注项未成功,请查看各注项状态",
+ "partialFailedOrderTitle": "部分注项失败",
+ "partialFailedOrderBody": "该订单中部分注项已成功、部分未成功;下方展示本注项结果。",
+ "lineFailedTitle": "本注项未成功",
"status": "状态",
"statusFilter": "状态筛选",
"noMore": "没有更多注单",
diff --git a/src/lib/player-datetime.ts b/src/lib/player-datetime.ts
index ec101c9..cfe7ece 100644
--- a/src/lib/player-datetime.ts
+++ b/src/lib/player-datetime.ts
@@ -53,6 +53,64 @@ function parseScheduleClockToMs(
);
}
+/** 将时刻格式化为指定 IANA 时区下的 `YYYY-MM-DD`(用于排期日筛选)。 */
+export function formatYmdInTimeZone(date: Date, timeZone: string): string {
+ try {
+ const parts = new Intl.DateTimeFormat("en-CA", {
+ timeZone,
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).formatToParts(date);
+ const map = new Map(parts.map((part) => [part.type, part.value]));
+ const year = map.get("year") ?? "0000";
+ const month = map.get("month") ?? "00";
+ const day = map.get("day") ?? "00";
+ return `${year}-${month}-${day}`;
+ } catch {
+ const y = date.getFullYear();
+ const m = String(date.getMonth() + 1).padStart(2, "0");
+ const d = String(date.getDate()).padStart(2, "0");
+ return `${y}-${m}-${d}`;
+ }
+}
+
+/** 排期时区下的「今天」`YYYY-MM-DD`。 */
+export function scheduleTodayYmd(timeZone: string): string {
+ return formatYmdInTimeZone(new Date(), timeZone);
+}
+
+/**
+ * 日期选择器单元格对应的排期日字面量(Y-M-D 与格子上显示的数字一致)。
+ * 与 API `start_date`/`end_date`(按排期时区解释)对齐。
+ */
+export function formatSchedulePickerYmd(date: Date): string {
+ const y = date.getFullYear();
+ const m = String(date.getMonth() + 1).padStart(2, "0");
+ const d = String(date.getDate()).padStart(2, "0");
+ return `${y}-${m}-${d}`;
+}
+
+export function parseSchedulePickerYmd(value: string): Date | undefined {
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
+ if (!m) return undefined;
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
+ return Number.isNaN(d.getTime()) ? undefined : d;
+}
+
+/** IANA 时区短标签(如 UTC、GMT+8)。 */
+export function getTimeZoneShortLabel(timeZone: string, date = new Date()): string {
+ try {
+ const parts = new Intl.DateTimeFormat(undefined, {
+ timeZone,
+ timeZoneName: "short",
+ }).formatToParts(date);
+ return parts.find((part) => part.type === "timeZoneName")?.value ?? timeZone;
+ } catch {
+ return timeZone;
+ }
+}
+
/** 浏览器本地时区短标签(如 CST、GMT+8),用于界面说明。 */
export function getBrowserTimeZoneLabel(date = new Date()): string {
try {
diff --git a/src/types/api/ticket-items.ts b/src/types/api/ticket-items.ts
index ba3b581..9cabb0d 100644
--- a/src/types/api/ticket-items.ts
+++ b/src/types/api/ticket-items.ts
@@ -3,6 +3,7 @@ import type { DrawResultListItem } from "@/types/api/draw-results";
export type TicketItemListRow = {
ticket_no: string;
order_no: string | null | undefined;
+ order_status?: string | null;
draw_no: string | null | undefined;
currency_code: string | null | undefined;
play_code: string;
@@ -34,6 +35,7 @@ export type TicketItemCombinationRow = {
export type TicketItemDetailPayload = {
ticket_no: string;
order_no: string | null | undefined;
+ order_status?: string | null;
draw_no: string | null | undefined;
currency_code: string | null | undefined;
play_code: string;
@@ -46,6 +48,8 @@ export type TicketItemDetailPayload = {
rebate_rate_snapshot: string;
actual_deduct_amount: number;
status: string;
+ fail_reason_code?: string | null;
+ fail_reason_text?: string | null;
win_amount: number;
jackpot_win_amount: number;
settled_at: string | null | undefined;
diff --git a/src/types/api/ticket.ts b/src/types/api/ticket.ts
index 8fe44bc..eff822f 100644
--- a/src/types/api/ticket.ts
+++ b/src/types/api/ticket.ts
@@ -73,9 +73,11 @@ export type TicketPlaceItem = {
export type TicketPlaceData = {
order_no: string;
- draw: { draw_id: string; status: string };
+ draw: { draw_id: string; status: string; db_status?: string };
summary: TicketPreviewData["summary"] & {
+ order_status?: string;
success_count?: number;
+ pending_confirm_count?: number;
failure_count?: number;
};
balance_after: number;
diff --git a/src/types/api/wallet-logs.ts b/src/types/api/wallet-logs.ts
index bbb8e61..9143e13 100644
--- a/src/types/api/wallet-logs.ts
+++ b/src/types/api/wallet-logs.ts
@@ -49,4 +49,6 @@ export type GetWalletLogsParams = {
size?: number;
/** 逗号分隔:transfer_in,transfer_out,bet,prize,refund,reversal */
type?: string;
+ /** 按钱包币种筛选 */
+ currency?: string;
};