feat: 增强注单处理逻辑并优化用户反馈体验
新增注单错误处理逻辑,支持已退款及待确认状态的异常场景处理。 更新 HallBetResultDialog:针对部分失败与已退款订单显示对应提示信息。 优化注单分组逻辑,新增订单状态处理,提升整体订单管理能力。 新增订单状态与用户通知相关多语言翻译,进一步提升用户体验。
This commit is contained in:
@@ -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<HTMLDivElement | null>(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() {
|
||||
/>
|
||||
<PopoverContent align="start" className="w-auto border-[#dce7f7] p-2 shadow-[0_16px_40px_rgba(15,23,42,0.14)]">
|
||||
<p className="mb-2 px-1 text-[11px] leading-snug text-muted-foreground">
|
||||
{t("orders.dateRangeHint")}
|
||||
{t("orders.dateRangeHint", { tz: scheduleTzLabel })}
|
||||
</p>
|
||||
<Calendar
|
||||
mode="range"
|
||||
@@ -276,11 +291,24 @@ export function TicketOrdersListScreen() {
|
||||
setToDate("");
|
||||
return;
|
||||
}
|
||||
setFromDate(range?.from ? formatYmd(range.from) : "");
|
||||
setToDate(range?.to ? formatYmd(range.to) : "");
|
||||
setFromDate(range?.from ? formatSchedulePickerYmd(range.from) : "");
|
||||
setToDate(range?.to ? formatSchedulePickerYmd(range.to) : "");
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 border-t px-2 py-1.5">
|
||||
<div className="flex items-center justify-between gap-2 border-t px-2 py-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
const today = scheduleTodayYmd(scheduleTimezone);
|
||||
setFromDate(today);
|
||||
setToDate(today);
|
||||
}}
|
||||
>
|
||||
{t("orders.scheduleToday")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -295,6 +323,7 @@ export function TicketOrdersListScreen() {
|
||||
<Button type="button" variant="secondary" size="xs" onClick={() => setRangeOpen(false)}>
|
||||
{t("actions.done")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -437,6 +466,11 @@ export function TicketOrdersListScreen() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{group.status === "partial_failed" ? (
|
||||
<p className="mt-2 text-xs font-bold text-amber-700">
|
||||
{t("orders.partialFailedHint")}
|
||||
</p>
|
||||
) : null}
|
||||
{totalWin > 0 && group.status === "settled_win" ? (
|
||||
<p className="mt-2 text-xs font-bold text-emerald-600">
|
||||
{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
|
||||
@@ -498,11 +532,11 @@ export function TicketOrdersListScreen() {
|
||||
{t("actions.next")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
) : lastPage > 1 ? (
|
||||
<p className="py-2 text-center text-xs text-slate-400">
|
||||
{t("orders.noMore")}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user