feat: 增强结果展示与用户交互

- 在 PlayerBottomNav 中新增注单导航选项
- 在 DrawResultDetailScreen 中添加高亮显示用户命中号码的功能,并显示个人派彩信息
- 在 DrawResultsListScreen 中引入 JackpotResultsStrip 组件以展示奖池信息
- 在 TwentyThreeResultsGrid 中实现命中号码的高亮效果,提升用户体验
This commit is contained in:
2026-05-11 15:40:42 +08:00
parent 1922a29f49
commit 377e03e167
16 changed files with 922 additions and 17 deletions

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { getDrawResultByNo } from "@/api/draw";
import { getTicketDrawMyMatch } from "@/api/ticket-items";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
@@ -13,8 +14,12 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
import { getPlayerBearerTokenPayload } from "@/lib/lottery-auth";
import { formatLotteryInstant } from "@/lib/player-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import { norm4d } from "@/lib/norm-4d";
import { cn } from "@/lib/utils";
import type { DrawResultDetailPayload } from "@/types/api/draw-results";
@@ -22,11 +27,17 @@ type DrawResultDetailScreenProps = {
drawNo: string;
};
/** §4.6 开奖结果详情23 分区 + [< >] 切换 */
/** §4.6 开奖结果详情23 分区 + [< >] 切换 + 本人命中高亮 + Jackpot */
export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) {
const [data, setData] = useState<DrawResultDetailPayload | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [highlightSet, setHighlightSet] = useState<ReadonlySet<string> | null>(null);
const [myTotals, setMyTotals] = useState<{
win: number;
jackpot: number;
hasBets: boolean;
} | null>(null);
const load = useCallback(async () => {
setLoading(true);
@@ -48,6 +59,43 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
});
}, [load]);
useEffect(() => {
let cancelled = false;
queueMicrotask(() => {
if (!data) {
setHighlightSet(null);
setMyTotals(null);
return;
}
const token = getPlayerBearerTokenPayload();
if (!token) {
setHighlightSet(new Set());
setMyTotals(null);
return;
}
void (async () => {
try {
const m = await getTicketDrawMyMatch(data.draw_no);
if (cancelled) return;
setHighlightSet(new Set(m.hit_numbers_4d.map((n) => norm4d(n))));
setMyTotals({
win: m.total_win_minor,
jackpot: m.total_jackpot_win_minor,
hasBets: m.has_bets,
});
} catch {
if (!cancelled) {
setHighlightSet(new Set());
setMyTotals(null);
}
}
})();
});
return () => {
cancelled = true;
};
}, [data]);
if (loading) {
return (
<Card>
@@ -81,8 +129,23 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
);
}
const currency = "NPR";
const showMyPayout =
myTotals &&
myTotals.hasBets &&
(myTotals.win > 0 || myTotals.jackpot > 0);
const showHitOnly =
myTotals?.hasBets &&
highlightSet &&
highlightSet.size > 0 &&
myTotals &&
myTotals.win === 0 &&
myTotals.jackpot === 0;
return (
<div className="flex flex-col gap-4">
<JackpotResultsStrip currencyCode={currency} />
<Card>
<CardHeader className="space-y-3 pb-2">
<div className="flex flex-wrap items-center justify-between gap-2">
@@ -120,11 +183,45 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
</CardDescription>
</CardHeader>
<CardContent className="pt-2">
<TwentyThreeResultsGrid numbers={data.results} />
<p className="mt-4 text-xs text-muted-foreground">
docs/06 §11.7§14.3
3 §4.6
</p>
<TwentyThreeResultsGrid
numbers={data.results}
highlighted4d={highlightSet ?? undefined}
/>
{showMyPayout && myTotals ? (
<div className="mt-4 rounded-md border border-emerald-500/25 bg-emerald-500/5 px-3 py-2 text-sm">
<p className="font-medium text-emerald-900 dark:text-emerald-100"></p>
<p className="mt-1 font-mono text-xs tabular-nums text-muted-foreground">
{formatMinorAsCurrency(myTotals.win, currency)}
{myTotals.jackpot > 0 ? (
<>
{" "}
· Jackpot{formatMinorAsCurrency(myTotals.jackpot, currency)}
</>
) : null}
</p>
</div>
) : null}
{showHitOnly ? (
<p className="mt-3 text-xs text-amber-900/90 dark:text-amber-100/90">
</p>
) : null}
<div className="mt-4 flex flex-col gap-3">
<p className="text-xs text-muted-foreground">
</p>
<Link
href={`/orders?draw_no=${encodeURIComponent(data.draw_no)}`}
className={cn(
buttonVariants({ variant: "default", size: "sm" }),
"w-full sm:w-auto sm:self-start",
)}
>
</Link>
</div>
</CardContent>
</Card>
</div>