feat: 增强结果展示与用户交互
- 在 PlayerBottomNav 中新增注单导航选项 - 在 DrawResultDetailScreen 中添加高亮显示用户命中号码的功能,并显示个人派彩信息 - 在 DrawResultsListScreen 中引入 JackpotResultsStrip 组件以展示奖池信息 - 在 TwentyThreeResultsGrid 中实现命中号码的高亮效果,提升用户体验
This commit is contained in:
196
src/features/orders/ticket-orders-list-screen.tsx
Normal file
196
src/features/orders/ticket-orders-list-screen.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getTicketItems } from "@/api/ticket-items";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { playLabelZh } from "@/lib/play-labels";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TicketItemListRow } from "@/types/api/ticket-items";
|
||||
|
||||
/** 界面文档 §4.7 我的注单 */
|
||||
export function TicketOrdersListScreen() {
|
||||
const searchParams = useSearchParams();
|
||||
const drawNoFilter = useMemo(
|
||||
() => (searchParams.get("draw_no") ?? "").trim(),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<TicketItemListRow[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [lastPage, setLastPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (nextPage: number, append: boolean) => {
|
||||
if (append) setLoadingMore(true);
|
||||
else setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await getTicketItems({
|
||||
page: nextPage,
|
||||
per_page: 20,
|
||||
draw_no: drawNoFilter || undefined,
|
||||
});
|
||||
setItems((prev) => (append ? [...prev, ...res.items] : res.items));
|
||||
setPage(res.page);
|
||||
setLastPage(res.last_page);
|
||||
setTotal(res.total);
|
||||
} catch {
|
||||
setError("加载失败");
|
||||
if (!append) setItems([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[drawNoFilter],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void fetchPage(1, false);
|
||||
});
|
||||
}, [fetchPage]);
|
||||
|
||||
const loadMore = () => {
|
||||
if (page >= lastPage || loadingMore) return;
|
||||
void fetchPage(page + 1, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">我的注单</CardTitle>
|
||||
<CardDescription>
|
||||
{drawNoFilter ? (
|
||||
<>
|
||||
当前筛选期号{" "}
|
||||
<span className="font-mono text-foreground">{drawNoFilter}</span>
|
||||
{" · "}
|
||||
<Link href="/orders" className="text-primary underline-offset-4 hover:underline">
|
||||
清除筛选
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
"最近下注记录"
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-24 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="border-destructive/40">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">注单</CardTitle>
|
||||
<CardDescription>{error}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button type="button" size="sm" onClick={() => void fetchPage(1, false)}>
|
||||
重试
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : items.length === 0 ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">还没有下注记录</CardTitle>
|
||||
<CardDescription>去下注大厅试试手气吧</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link href="/hall" className={cn(buttonVariants({ size: "sm" }))}>
|
||||
去下注
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground">共 {total} 条</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
{items.map((row) => {
|
||||
const cur = row.currency_code ?? "NPR";
|
||||
const st = ticketStatusDisplay(
|
||||
row.status,
|
||||
row.win_amount,
|
||||
row.jackpot_win_amount,
|
||||
);
|
||||
const totalWin = row.win_amount + row.jackpot_win_amount;
|
||||
return (
|
||||
<Link key={row.ticket_no} href={`/orders/${encodeURIComponent(row.ticket_no)}`}>
|
||||
<Card className="transition-colors hover:border-primary/30">
|
||||
<CardHeader className="space-y-1 pb-2">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<span className="font-mono text-sm font-semibold text-foreground">
|
||||
{row.draw_no ?? "—"}
|
||||
</span>
|
||||
<StatusDot label={st.label} dotClass={st.dotClass} ring={st.ring} />
|
||||
</div>
|
||||
<CardDescription className="font-mono text-xs leading-relaxed">
|
||||
号码 {row.original_number ?? row.play_code} · 玩法 {playLabelZh(row.play_code)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 pt-0 text-xs">
|
||||
<p className="text-muted-foreground">
|
||||
金额 {formatMinorAsCurrency(row.total_bet_amount, cur)} · 实扣{" "}
|
||||
{formatMinorAsCurrency(row.actual_deduct_amount, cur)}
|
||||
</p>
|
||||
{totalWin > 0 && row.status === "settled_win" ? (
|
||||
<p className="font-medium text-emerald-700 dark:text-emerald-400">
|
||||
中奖 {formatMinorAsCurrency(totalWin, cur)}
|
||||
{row.jackpot_win_amount > 0 ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
(含 Jackpot {formatMinorAsCurrency(row.jackpot_win_amount, cur)})
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{formatLotteryInstant(row.placed_at ?? null)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{page < lastPage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
disabled={loadingMore}
|
||||
onClick={() => loadMore()}
|
||||
>
|
||||
{loadingMore ? "加载中…" : "加载更多"}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user