feat: 新增玩法规则页并增强注单筛选与结果展示
This commit is contained in:
@@ -10,6 +10,9 @@ export function ticketStatusDisplay(
|
||||
if (status === "success") {
|
||||
return { label: t?.("ticketStatus.success") ?? "待开奖", dotClass: "bg-sky-500" };
|
||||
}
|
||||
if (status === "pending_payout") {
|
||||
return { label: t?.("ticketStatus.pending_payout") ?? "已中奖待派彩", dotClass: "bg-amber-500" };
|
||||
}
|
||||
if (status === "settled_win" && total > 0) {
|
||||
return { label: t?.("ticketStatus.settled_win") ?? "已派彩", dotClass: "bg-emerald-500" };
|
||||
}
|
||||
|
||||
@@ -42,6 +42,27 @@ function formatOddsSnapshot(
|
||||
return parts.length ? parts.join(" · ") : "—";
|
||||
}
|
||||
|
||||
type TimelineRow = {
|
||||
code: string;
|
||||
label: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
type TicketItemDetailWithExtras = TicketItemDetailPayload & {
|
||||
timeline?: TimelineRow[];
|
||||
match_result?: {
|
||||
matched?: boolean;
|
||||
matched_prize_tier?: string | null;
|
||||
win_amount_minor?: number;
|
||||
jackpot_allocation_minor?: number;
|
||||
lines?: Array<{
|
||||
number_4d?: string | null;
|
||||
matched_tier?: string | null;
|
||||
payout?: number | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
/** 界面文档 §4.8 注单详情 */
|
||||
export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
const { t } = useTranslation("player");
|
||||
@@ -145,6 +166,10 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
defaultValue: data.settlement.matched_prize_tier,
|
||||
})
|
||||
: null;
|
||||
const extras = data as TicketItemDetailWithExtras;
|
||||
const timeline = extras.timeline ?? [];
|
||||
const matchResult = extras.match_result;
|
||||
const hasSettlement = data.settlement !== null || data.status === "settled_win" || data.status === "settled_lose";
|
||||
|
||||
return (
|
||||
<PlayerPanel
|
||||
@@ -221,34 +246,42 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{pub?.results ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-bold text-[#0b3f96]">{t("orders.drawNumbers")}</p>
|
||||
<TwentyThreeResultsGrid numbers={pub.results} highlighted4d={highlight} />
|
||||
{first ? (
|
||||
<p className="text-xs text-slate-500">
|
||||
{t("orders.firstPrize")}{" "}
|
||||
<span className="font-mono font-semibold text-slate-900">{first}</span>
|
||||
{comboHits.length > 0 ? (
|
||||
<span className="font-semibold text-emerald-600">
|
||||
{" "}
|
||||
← {t("orders.hit")}
|
||||
</span>
|
||||
) : null}
|
||||
{pub?.results ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-bold text-[#0b3f96]">{t("orders.drawNumbers")}</p>
|
||||
<TwentyThreeResultsGrid numbers={pub.results} highlighted4d={highlight} />
|
||||
{first ? (
|
||||
<p className="text-xs text-slate-500">
|
||||
{t("orders.firstPrize")}{" "}
|
||||
<span className="font-mono font-semibold text-slate-900">{first}</span>
|
||||
{comboHits.length > 0 ? (
|
||||
<span className="font-semibold text-emerald-600">
|
||||
{" "}
|
||||
← {t("orders.hit")}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
{!hasSettlement ? (
|
||||
<p className="rounded-lg border border-[#dce7f7] bg-[#f8fbff] px-3 py-2 text-xs text-[#32518d]">
|
||||
{t("orders.matchPendingSettlement", { defaultValue: "已开奖,等待系统结算后显示中奖结果。" })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-[#dce7f7] bg-[#f8fbff] px-3 py-3 text-xs">
|
||||
<p className="font-bold text-[#0b3f96]">{t("orders.drawNumbers")}</p>
|
||||
<p className="mt-1 text-[#32518d]">
|
||||
{t("orders.drawPendingMatch", { defaultValue: "本期开奖号码尚未发布,暂不能判断是否中奖。" })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-lg border border-[#dce7f7] bg-[#f8fbff] px-3 py-2 text-xs text-[#32518d]">
|
||||
{t("orders.notPublished")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.settlement && tierLabel ? (
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs">
|
||||
<p className="font-bold text-emerald-900">
|
||||
{t("orders.matchWin", { tier: tierLabel })}
|
||||
</p>
|
||||
{data.settlement && tierLabel ? (
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs">
|
||||
<p className="font-bold text-emerald-900">
|
||||
{t("orders.matchWin", { tier: tierLabel })}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-emerald-800/90">
|
||||
{t("orders.winAmount", {
|
||||
amount: formatMinorAsCurrency(data.settlement.win_amount_minor, cur),
|
||||
@@ -269,10 +302,53 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
<p className="mt-1 font-mono font-semibold text-emerald-900">
|
||||
{t("orders.payoutTotal", { amount: formatMinorAsCurrency(totalWin, cur) })}
|
||||
</p>
|
||||
</div>
|
||||
) : data.status === "settled_lose" ? (
|
||||
<p className="text-xs text-slate-500">{t("orders.matchLose")}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : hasSettlement ? (
|
||||
<p className="text-xs text-slate-500">{t("orders.matchLose")}</p>
|
||||
) : null}
|
||||
|
||||
{matchResult && hasSettlement ? (
|
||||
<div className="rounded-lg border border-[#dce7f7] bg-[#f8fbff] px-3 py-3 text-xs">
|
||||
<p className="font-bold text-[#0b3f96]">
|
||||
{t("orders.matchResult", { defaultValue: "匹配结果" })}
|
||||
</p>
|
||||
<p className="mt-1 text-slate-600">
|
||||
{matchResult.matched
|
||||
? t("orders.matchWin", { tier: tierLabel ?? (matchResult.matched_prize_tier ?? "—") })
|
||||
: t("orders.matchLose")}
|
||||
</p>
|
||||
{Array.isArray(matchResult.lines) && matchResult.lines.length > 0 ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
{matchResult.lines.map((line, idx) => (
|
||||
<p key={`${line.number_4d ?? "line"}-${idx}`} className="font-mono text-[11px] text-slate-500">
|
||||
{line.number_4d ?? "—"} · {line.matched_tier ?? "—"} · {formatMinorAsCurrency(line.payout ?? 0, cur)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{timeline.length > 0 ? (
|
||||
<div className="rounded-xl border border-[#e8eef7] bg-[#f8fbff] px-3 py-3">
|
||||
<p className="text-sm font-bold text-[#0b3f96]">
|
||||
{t("orders.timeline", { defaultValue: "时间线" })}
|
||||
</p>
|
||||
<div className="mt-2 space-y-2">
|
||||
{timeline.map((row) => (
|
||||
<div key={row.code} className="flex items-start justify-between gap-3 rounded-lg bg-white px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-bold text-[#32518d]">{row.label}</p>
|
||||
<p className="mt-0.5 font-mono text-[11px] text-slate-500">{row.code}</p>
|
||||
</div>
|
||||
<p className="shrink-0 font-mono text-[11px] text-slate-500">
|
||||
{formatLotteryInstant(row.time)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{data.settled_at ? (
|
||||
<p className="text-[11px] text-slate-500">
|
||||
|
||||
@@ -3,27 +3,46 @@
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CalendarRange, ChevronDown, Search } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getTicketItems } from "@/api/ticket-items";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||
import { playLabel } from "@/lib/play-labels";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TicketItemListRow } from "@/types/api/ticket-items";
|
||||
|
||||
const ORDERS_PAGE_SIZE = 10;
|
||||
const ORDERS_PAGE_SIZE = 20;
|
||||
const STATUS_OPTIONS = ["success", "settled_win", "settled_lose", "failed"] 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");
|
||||
const drawNoFilter = useMemo(
|
||||
() => (searchParams.get("draw_no") ?? "").trim(),
|
||||
[searchParams],
|
||||
);
|
||||
const drawNoFilter = useMemo(() => (searchParams.get("draw_no") ?? "").trim(), [searchParams]);
|
||||
|
||||
const [items, setItems] = useState<TicketItemListRow[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -32,7 +51,39 @@ export function TicketOrdersListScreen() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [queryDrawNo, setQueryDrawNo] = useState(drawNoFilter);
|
||||
const [queryNumber, setQueryNumber] = useState("");
|
||||
const [queryStatuses, setQueryStatuses] = useState<string[]>([]);
|
||||
const [fromDate, setFromDate] = useState("");
|
||||
const [toDate, setToDate] = useState("");
|
||||
const [rangeOpen, setRangeOpen] = useState(false);
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
const [calendarMonth, setCalendarMonth] = useState(() => new Date());
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
const isMobile = useIsMobile();
|
||||
const initialLoadDone = useRef(false);
|
||||
|
||||
const selectedRange = useMemo(() => {
|
||||
const from = parseYmd(fromDate);
|
||||
const to = parseYmd(toDate);
|
||||
if (!from && !to) return undefined;
|
||||
if (from && to) return { from, to };
|
||||
if (from) return { from };
|
||||
return to ? { from: to } : undefined;
|
||||
}, [fromDate, toDate]);
|
||||
|
||||
const formatCompactDate = useCallback((value: string) => {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||
if (!m) return value;
|
||||
return `${m[2]}-${m[3]}`;
|
||||
}, []);
|
||||
|
||||
const dateLabel = useMemo(() => {
|
||||
if (!fromDate && !toDate) return t("orders.dateRange", { defaultValue: "日期范围" });
|
||||
if (fromDate && toDate) return `${formatCompactDate(fromDate)} ~ ${formatCompactDate(toDate)}`;
|
||||
return `${fromDate ? formatCompactDate(fromDate) : "..." } ~ ${toDate ? formatCompactDate(toDate) : "..."}`;
|
||||
}, [formatCompactDate, fromDate, t, toDate]);
|
||||
const visiblePages = useMemo(() => buildPageWindow(page, lastPage), [lastPage, page]);
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (nextPage: number, append: boolean) => {
|
||||
@@ -43,7 +94,11 @@ export function TicketOrdersListScreen() {
|
||||
const res = await getTicketItems({
|
||||
page: nextPage,
|
||||
per_page: ORDERS_PAGE_SIZE,
|
||||
draw_no: drawNoFilter || undefined,
|
||||
draw_no: queryDrawNo || drawNoFilter || undefined,
|
||||
number: queryNumber || undefined,
|
||||
status: queryStatuses.length ? queryStatuses : undefined,
|
||||
start_date: fromDate || undefined,
|
||||
end_date: toDate || undefined,
|
||||
});
|
||||
setItems((prev) => (append ? [...prev, ...res.items] : res.items));
|
||||
setPage(res.page);
|
||||
@@ -57,19 +112,20 @@ export function TicketOrdersListScreen() {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[drawNoFilter, t],
|
||||
[drawNoFilter, fromDate, queryDrawNo, queryNumber, queryStatuses, t, toDate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
if (!initialLoadDone.current) {
|
||||
initialLoadDone.current = true;
|
||||
void fetchPage(1, false);
|
||||
});
|
||||
}, [fetchPage]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (page >= lastPage || loadingMore) return;
|
||||
void fetchPage(page + 1, true);
|
||||
}, [fetchPage, lastPage, loadingMore, page]);
|
||||
return;
|
||||
}
|
||||
setItems([]);
|
||||
setPage(1);
|
||||
setLastPage(1);
|
||||
void fetchPage(1, false);
|
||||
}, [fetchPage, queryDrawNo, queryNumber, queryStatuses, fromDate, toDate]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = loadMoreRef.current;
|
||||
@@ -78,7 +134,7 @@ export function TicketOrdersListScreen() {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
loadMore();
|
||||
void fetchPage(page + 1, true);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "160px" },
|
||||
@@ -86,14 +142,19 @@ export function TicketOrdersListScreen() {
|
||||
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, [lastPage, loadMore, loading, loadingMore, page]);
|
||||
}, [fetchPage, lastPage, loading, loadingMore, page]);
|
||||
|
||||
return (
|
||||
<PlayerPanel title={t("orders.title")} subtitle={t("orders.subtitle")} eyebrow={t("brand.name")}>
|
||||
<PlayerPanel
|
||||
title={t("orders.title")}
|
||||
subtitle={t("orders.subtitle")}
|
||||
eyebrow={t("brand.name")}
|
||||
containerClassName="max-w-[720px]"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-[#e6edf8] bg-[#f8fbff] p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="rounded-xl border border-[#e6edf8] bg-white p-3 shadow-[0_8px_24px_rgba(15,23,42,0.04)] sm:p-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-bold text-[#32518d]">
|
||||
{drawNoFilter ? t("orders.filteredIssue") : t("orders.totalRecords")}
|
||||
</p>
|
||||
@@ -101,21 +162,158 @@ export function TicketOrdersListScreen() {
|
||||
{drawNoFilter || total}
|
||||
</p>
|
||||
</div>
|
||||
{drawNoFilter ? (
|
||||
<Link
|
||||
href="/orders"
|
||||
className="shrink-0 rounded-full border border-[#dce7f7] bg-white px-3 py-1.5 text-xs font-bold text-[#0b56b7]"
|
||||
>
|
||||
{t("actions.clear")}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex shrink-0 items-center gap-2 self-start sm:self-auto">
|
||||
<Link
|
||||
href="/hall"
|
||||
className="shrink-0 rounded-full bg-[#e5002c] px-3 py-1.5 text-xs font-bold text-white"
|
||||
className="inline-flex h-10 shrink-0 items-center rounded-full bg-[#e5002c] px-5 text-sm font-bold text-white"
|
||||
>
|
||||
{t("orders.betNow")}
|
||||
</Link>
|
||||
)}
|
||||
{(queryDrawNo || queryNumber || fromDate || toDate || queryStatuses.length > 0) ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-10 rounded-full border-[#dce7f7] bg-white px-4 text-sm font-semibold text-[#32518d] hover:bg-[#f8fbff]"
|
||||
onClick={() => {
|
||||
setQueryDrawNo(drawNoFilter);
|
||||
setQueryNumber("");
|
||||
setFromDate("");
|
||||
setToDate("");
|
||||
setQueryStatuses([]);
|
||||
setRangeOpen(false);
|
||||
setStatusOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("actions.clear")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 min-[420px]:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="flex h-10 items-center rounded-full border border-[#dce7f7] bg-[#fbfdff] px-3">
|
||||
<Input
|
||||
value={queryDrawNo}
|
||||
onChange={(e) => setQueryDrawNo(e.target.value)}
|
||||
placeholder={t("orders.drawNo")}
|
||||
aria-label={t("orders.drawNo")}
|
||||
className="h-8 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-10 items-center gap-2 rounded-full border border-[#dce7f7] bg-[#fbfdff] px-3">
|
||||
<Search className="size-3.5 shrink-0 text-slate-400" />
|
||||
<Input
|
||||
value={queryNumber}
|
||||
onChange={(e) => setQueryNumber(e.target.value)}
|
||||
placeholder={t("orders.number")}
|
||||
aria-label={t("orders.number")}
|
||||
className="h-8 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Popover open={rangeOpen} onOpenChange={setRangeOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-10 w-full justify-start gap-2 rounded-full border-[#dce7f7] bg-[#fbfdff] px-3 text-left text-sm font-semibold text-[#32518d] hover:bg-white"
|
||||
>
|
||||
<CalendarRange className="size-4 text-[#7890b8]" />
|
||||
<span className="truncate">{dateLabel}</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="start" className="w-auto border-[#dce7f7] p-2 shadow-[0_16px_40px_rgba(15,23,42,0.14)]">
|
||||
<Calendar
|
||||
mode="range"
|
||||
month={calendarMonth}
|
||||
onMonthChange={setCalendarMonth}
|
||||
numberOfMonths={isMobile ? 1 : 2}
|
||||
selected={selectedRange}
|
||||
onSelect={(range) => {
|
||||
if (!range?.from && !range?.to) {
|
||||
setFromDate("");
|
||||
setToDate("");
|
||||
return;
|
||||
}
|
||||
setFromDate(range?.from ? formatYmd(range.from) : "");
|
||||
setToDate(range?.to ? formatYmd(range.to) : "");
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 border-t px-2 py-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setFromDate("");
|
||||
setToDate("");
|
||||
}}
|
||||
>
|
||||
清除
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" size="xs" onClick={() => setRangeOpen(false)}>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-10 w-full justify-between gap-2 rounded-full border-[#dce7f7] bg-[#fbfdff] px-3 text-sm font-semibold text-[#32518d] hover:bg-white"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="shrink-0">状态</span>
|
||||
{queryStatuses.length ? (
|
||||
<span className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[#0b56b7] px-1 text-[10px] font-black text-white">
|
||||
{queryStatuses.length}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-[#7890b8]">
|
||||
<ChevronDown className="size-3.5" />
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="start" className="w-56 border-[#dce7f7] p-2 shadow-[0_16px_40px_rgba(15,23,42,0.14)]">
|
||||
<div className="space-y-1">
|
||||
<p className="px-1 pb-1 text-[11px] font-bold text-[#32518d]">
|
||||
{t("orders.statusFilter", { defaultValue: "状态筛选" })}
|
||||
</p>
|
||||
{STATUS_OPTIONS.map((status) => {
|
||||
const checked = queryStatuses.includes(status);
|
||||
return (
|
||||
<button
|
||||
key={status}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs font-semibold transition-colors",
|
||||
checked ? "bg-[#eaf2ff] text-[#0b56b7]" : "text-[#32518d] hover:bg-[#f8fbff]",
|
||||
)}
|
||||
onClick={() => {
|
||||
setQueryStatuses((current) =>
|
||||
current.includes(status)
|
||||
? current.filter((s) => s !== status)
|
||||
: [...current, status],
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Checkbox className="size-3.5" checked={checked} />
|
||||
<span className="truncate">{t(`ticketStatus.${status}`, { defaultValue: status })}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,12 +350,7 @@ export function TicketOrdersListScreen() {
|
||||
<div className="space-y-3">
|
||||
{items.map((row) => {
|
||||
const cur = row.currency_code ?? "NPR";
|
||||
const st = ticketStatusDisplay(
|
||||
row.status,
|
||||
row.win_amount,
|
||||
row.jackpot_win_amount,
|
||||
t,
|
||||
);
|
||||
const st = ticketStatusDisplay(row.status, row.win_amount, row.jackpot_win_amount, t);
|
||||
const totalWin = row.win_amount + row.jackpot_win_amount;
|
||||
return (
|
||||
<Link
|
||||
@@ -178,17 +371,13 @@ export function TicketOrdersListScreen() {
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">
|
||||
{t("orders.stake")}
|
||||
</p>
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
|
||||
<p className="mt-1 text-sm font-black text-slate-900">
|
||||
{formatMinorAsCurrency(row.total_bet_amount, cur)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">
|
||||
{t("orders.deduction")}
|
||||
</p>
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
|
||||
<p className="mt-1 text-sm font-black text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(row.actual_deduct_amount, cur)}
|
||||
</p>
|
||||
@@ -206,19 +395,58 @@ export function TicketOrdersListScreen() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div ref={loadMoreRef} className="min-h-1" />
|
||||
{page < lastPage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-10 w-full rounded-xl border-[#dce7f7] bg-white text-sm font-bold text-[#32518d] hover:bg-[#f8fbff]"
|
||||
disabled={loadingMore}
|
||||
onClick={loadMore}
|
||||
>
|
||||
{loadingMore
|
||||
? t("actions.loading", { defaultValue: "加载中..." })
|
||||
: t("actions.loadMore", { defaultValue: "加载更多" })}
|
||||
</Button>
|
||||
{isMobile ? <div ref={loadMoreRef} className="min-h-1" /> : null}
|
||||
{isMobile && page < lastPage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-10 w-full rounded-xl border-[#dce7f7] bg-white text-sm font-bold text-[#32518d] hover:bg-[#f8fbff]"
|
||||
disabled={loadingMore}
|
||||
onClick={() => void fetchPage(page + 1, true)}
|
||||
>
|
||||
{loadingMore ? t("actions.loading", { defaultValue: "加载中..." }) : t("actions.loadMore", { defaultValue: "加载更多" })}
|
||||
</Button>
|
||||
) : !isMobile && lastPage > 1 ? (
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 rounded-xl border border-[#e6edf8] bg-white px-3 py-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full border-[#dce7f7] bg-white text-[#32518d]"
|
||||
disabled={loading || page <= 1}
|
||||
onClick={() => void fetchPage(Math.max(1, page - 1), false)}
|
||||
>
|
||||
{t("actions.previous", { defaultValue: "上一页" })}
|
||||
</Button>
|
||||
{visiblePages.map((p) => (
|
||||
<Button
|
||||
key={p}
|
||||
type="button"
|
||||
variant={p === page ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"min-w-8 rounded-full",
|
||||
p === page
|
||||
? "bg-[#07459f] text-white hover:bg-[#063b88]"
|
||||
: "border-[#dce7f7] bg-white text-[#32518d]",
|
||||
)}
|
||||
disabled={loading}
|
||||
onClick={() => void fetchPage(p, false)}
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full border-[#dce7f7] bg-white text-[#32518d]"
|
||||
disabled={loading || page >= lastPage}
|
||||
onClick={() => void fetchPage(Math.min(lastPage, page + 1), false)}
|
||||
>
|
||||
{t("actions.next", { defaultValue: "下一页" })}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-2 text-center text-xs text-slate-400">
|
||||
{t("orders.noMore", { defaultValue: "没有更多注单" })}
|
||||
@@ -230,3 +458,12 @@ export function TicketOrdersListScreen() {
|
||||
</PlayerPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function buildPageWindow(current: number, last: number): number[] {
|
||||
if (last <= 5) {
|
||||
return Array.from({ length: last }, (_, index) => index + 1);
|
||||
}
|
||||
|
||||
const start = Math.max(1, Math.min(current - 2, last - 4));
|
||||
return Array.from({ length: 5 }, (_, index) => start + index);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user