refactor: 重构整体页面布局与样式,统一UI设计风格

- 重构PlayerAppShell,移除冗余头部导航与国际化依赖,统一页面背景与内边距
- 新增通用页面容器组件PlayerPanel,统一页面头部布局与样式
- 重构底部导航栏,调整图标、文案与样式,新增激活状态指示器
- 重构所有页面组件:大厅页、注单页、结果页、开奖面板等,统一使用新的UI组件与设计风格
- 优化状态标签、卡片、按钮等组件的视觉样式,统一配色与圆角规范
- 移除冗余依赖与注释代码,整理代码结构
This commit is contained in:
2026-05-14 11:18:08 +08:00
parent f777888940
commit ac612cb32c
11 changed files with 1054 additions and 735 deletions

View File

@@ -32,7 +32,7 @@ export function StatusDot({
ring?: boolean;
}) {
return (
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[#e5edf8] bg-[#f8fbff] px-2 py-1 text-xs font-bold text-slate-600">
<span
className={cn(
"inline-block size-2 shrink-0 rounded-full",
@@ -41,7 +41,7 @@ export function StatusDot({
)}
aria-hidden
/>
<span className="text-foreground">{label}</span>
<span>{label}</span>
</span>
);
}

View File

@@ -5,23 +5,15 @@ 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 { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { PlayerPanel } from "@/components/layout/player-panel";
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
import { formatLotteryInstant } from "@/lib/player-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import { formatLotteryInstant } from "@/lib/player-datetime";
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(
@@ -75,122 +67,132 @@ export function TicketOrdersListScreen() {
};
return (
<div className="flex flex-col gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base"></CardTitle>
<CardDescription>
<PlayerPanel title="My Bets" subtitle="Recent ticket records" eyebrow="N lotto">
<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">
<p className="text-xs font-bold text-[#32518d]">
{drawNoFilter ? "Filtered Issue" : "Total Records"}
</p>
<p className="mt-1 truncate font-mono text-lg font-black text-[#0b3f96]">
{drawNoFilter || total}
</p>
</div>
{drawNoFilter ? (
<>
{" "}
<span className="font-mono text-foreground">{drawNoFilter}</span>
{" · "}
<Link href="/orders" className="text-primary underline-offset-4 hover:underline">
</Link>
</>
<Link
href="/orders"
className="shrink-0 rounded-full border border-[#dce7f7] bg-white px-3 py-1.5 text-xs font-bold text-[#0b56b7]"
>
Clear
</Link>
) : (
"最近下注记录"
<Link
href="/hall"
className="shrink-0 rounded-full bg-[#e5002c] px-3 py-1.5 text-xs font-bold text-white"
>
Bet Now
</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 ? (
</div>
{loading ? (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-28 w-full rounded-xl" />
))}
</div>
) : error ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-4 text-sm text-red-700">
<p>{error}</p>
<Button
type="button"
variant="secondary"
size="sm"
className="w-full"
disabled={loadingMore}
onClick={() => loadMore()}
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
onClick={() => void fetchPage(1, false)}
>
{loadingMore ? "加载中…" : "加载更多"}
Retry
</Button>
) : null}
</>
)}
</div>
</div>
) : items.length === 0 ? (
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-[#f8fbff] px-4 py-10 text-center">
<p className="text-sm font-bold text-slate-700">No bet records yet.</p>
<Link
href="/hall"
className="mt-4 inline-flex h-9 items-center rounded-lg bg-[#e5002c] px-4 text-sm font-bold text-white"
>
Submit Bet
</Link>
</div>
) : (
<>
<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,
);
const totalWin = row.win_amount + row.jackpot_win_amount;
return (
<Link
key={row.ticket_no}
href={`/orders/${encodeURIComponent(row.ticket_no)}`}
className="block rounded-xl border border-[#e5edf8] bg-white p-3 shadow-[0_8px_24px_rgba(15,23,42,0.05)] transition-colors hover:border-[#b9ccf6]"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-mono text-sm font-black text-[#0b3f96]">
{row.draw_no ?? "—"}
</p>
<p className="mt-1 truncate text-xs text-slate-500">
{playLabelZh(row.play_code)} · {row.original_number ?? row.play_code}
</p>
</div>
<StatusDot label={st.label} dotClass={st.dotClass} ring={st.ring} />
</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]">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]">Deduction</p>
<p className="mt-1 text-sm font-black text-[#0b3f96]">
{formatMinorAsCurrency(row.actual_deduct_amount, cur)}
</p>
</div>
</div>
{totalWin > 0 && row.status === "settled_win" ? (
<p className="mt-2 text-xs font-bold text-emerald-600">
Win {formatMinorAsCurrency(totalWin, cur)}
</p>
) : null}
<p className="mt-2 text-[11px] text-slate-500">
{formatLotteryInstant(row.placed_at ?? null)}
</p>
</Link>
);
})}
</div>
{page < lastPage ? (
<Button
type="button"
size="sm"
className="h-10 w-full rounded-lg bg-[#07459f] text-white hover:bg-[#063b88]"
disabled={loadingMore}
onClick={() => loadMore()}
>
{loadingMore ? "Loading..." : "Load More"}
</Button>
) : null}
</>
)}
</div>
</PlayerPanel>
);
}