feat: 优化多语言文案接入并升级大厅与钱包交互体验

- 补充大厅下注结果、快速填单、查中奖、分页与通用操作多语言文案
- 移除多处界面默认文案回退,统一改用 i18n 配置输出
- 优化大厅快速填单、开奖结果入口与注单筛选区域视觉交互
- 重构钱包转入转出弹窗与流水卡片样式,增强金额与状态信息展示
This commit is contained in:
2026-05-20 17:56:18 +08:00
parent a2a29107f8
commit 626914feb6
20 changed files with 578 additions and 359 deletions

View File

@@ -8,17 +8,17 @@ export function ticketStatusDisplay(
): { label: string; dotClass: string; ring?: boolean } {
const total = winMinor + jackpotMinor;
if (status === "success") {
return { label: t?.("ticketStatus.success") ?? "待开奖", dotClass: "bg-sky-500" };
return { label: t?.("ticketStatus.success") ?? status, dotClass: "bg-sky-500" };
}
if (status === "pending_payout") {
return { label: t?.("ticketStatus.pending_payout") ?? "已中奖待派彩", dotClass: "bg-amber-500" };
return { label: t?.("ticketStatus.pending_payout") ?? status, dotClass: "bg-amber-500" };
}
if (status === "settled_win" && total > 0) {
return { label: t?.("ticketStatus.settled_win") ?? "已派彩", dotClass: "bg-emerald-500" };
return { label: t?.("ticketStatus.settled_win") ?? status, dotClass: "bg-emerald-500" };
}
if (status === "settled_lose" || status === "settled_win") {
return {
label: t?.("ticketStatus.settled_lose") ?? "未中奖",
label: t?.("ticketStatus.settled_lose") ?? status,
dotClass: "bg-background",
ring: true,
};

View File

@@ -264,7 +264,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
) : null}
{!hasSettlement ? (
<p className="rounded-lg border border-[#dce7f7] bg-[#f8fbff] px-3 py-2 text-xs text-[#32518d]">
{t("orders.matchPendingSettlement", { defaultValue: "已开奖,等待系统结算后显示中奖结果。" })}
{t("orders.matchPendingSettlement")}
</p>
) : null}
</div>
@@ -272,7 +272,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
<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: "本期开奖号码尚未发布,暂不能判断是否中奖。" })}
{t("orders.drawPendingMatch")}
</p>
</div>
)}
@@ -310,7 +310,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
{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: "匹配结果" })}
{t("orders.matchResult")}
</p>
<p className="mt-1 text-slate-600">
{matchResult.matched
@@ -332,7 +332,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
{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: "时间线" })}
{t("orders.timeline")}
</p>
<div className="mt-2 space-y-2">
{timeline.map((row) => (

View File

@@ -83,7 +83,7 @@ export function TicketOrdersListScreen() {
}, []);
const dateLabel = useMemo(() => {
if (!fromDate && !toDate) return t("orders.dateRange", { defaultValue: "日期范围" });
if (!fromDate && !toDate) return t("orders.dateRange");
if (fromDate && toDate) return `${formatCompactDate(fromDate)} ~ ${formatCompactDate(toDate)}`;
return `${fromDate ? formatCompactDate(fromDate) : "..." } ~ ${toDate ? formatCompactDate(toDate) : "..."}`;
}, [formatCompactDate, fromDate, t, toDate]);
@@ -156,20 +156,20 @@ export function TicketOrdersListScreen() {
containerClassName="max-w-[720px]"
>
<div className="space-y-4">
<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="rounded-2xl border border-[#dfe9f8] bg-white p-3 shadow-[0_10px_28px_rgba(15,23,42,0.05)] sm:p-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-xs font-bold text-[#32518d]">
<p className="text-[11px] font-black uppercase tracking-wide text-[#6f86ad]">
{drawNoFilter ? t("orders.filteredIssue") : t("orders.totalRecords")}
</p>
<p className="mt-1 truncate font-mono text-lg font-black text-[#0b3f96]">
<p className="mt-0.5 truncate font-mono text-2xl font-black leading-none text-[#0b3f96]">
{drawNoFilter || total}
</p>
</div>
<div className="flex shrink-0 items-center gap-2 self-start sm:self-auto">
<div className="flex shrink-0 items-center gap-2">
<Link
href="/hall"
className="inline-flex h-10 shrink-0 items-center rounded-full bg-[#e5002c] px-5 text-sm font-bold text-white"
className="inline-flex h-9 shrink-0 items-center rounded-full bg-[#e5002c] px-5 text-sm font-black text-white shadow-[0_8px_18px_rgba(229,0,44,0.22)]"
>
{t("orders.betNow")}
</Link>
@@ -177,7 +177,7 @@ export function TicketOrdersListScreen() {
<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]"
className="h-9 rounded-full border-[#dce7f7] bg-white px-3 text-xs font-bold text-[#32518d] hover:bg-[#f8fbff]"
onClick={() => {
setQueryDrawNo(drawNoFilter);
setQueryNumber("");
@@ -194,25 +194,25 @@ export function TicketOrdersListScreen() {
</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">
<div className="mt-3 grid grid-cols-2 gap-2 lg:grid-cols-4">
<div className="flex h-9 min-w-0 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"
className="h-7 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">
<div className="flex h-9 min-w-0 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"
className="h-7 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0"
/>
</div>
@@ -222,7 +222,7 @@ export function TicketOrdersListScreen() {
<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"
className="h-9 w-full justify-start gap-2 rounded-full border-[#dce7f7] bg-[#fbfdff] px-3 text-left text-sm font-bold text-[#32518d] hover:bg-white"
>
<CalendarRange className="size-4 text-[#7890b8]" />
<span className="truncate">{dateLabel}</span>
@@ -256,10 +256,10 @@ export function TicketOrdersListScreen() {
setToDate("");
}}
>
{t("actions.clear")}
</Button>
<Button type="button" variant="secondary" size="xs" onClick={() => setRangeOpen(false)}>
{t("actions.done")}
</Button>
</div>
</PopoverContent>
@@ -271,10 +271,10 @@ export function TicketOrdersListScreen() {
<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"
className="h-9 w-full justify-between gap-2 rounded-full border-[#dce7f7] bg-[#fbfdff] px-3 text-sm font-bold text-[#32518d] hover:bg-white"
>
<span className="flex min-w-0 items-center gap-1.5">
<span className="shrink-0"></span>
<span className="shrink-0">{t("orders.status")}</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}
@@ -290,7 +290,7 @@ export function TicketOrdersListScreen() {
<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: "状态筛选" })}
{t("orders.statusFilter")}
</p>
{STATUS_OPTIONS.map((status) => {
const checked = queryStatuses.includes(status);
@@ -408,7 +408,7 @@ export function TicketOrdersListScreen() {
disabled={loadingMore}
onClick={() => void fetchPage(page + 1, true)}
>
{loadingMore ? t("actions.loading", { defaultValue: "加载中..." }) : t("actions.loadMore", { defaultValue: "加载更多" })}
{loadingMore ? t("actions.loading") : t("actions.loadMore")}
</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">
@@ -420,7 +420,7 @@ export function TicketOrdersListScreen() {
disabled={loading || page <= 1}
onClick={() => void fetchPage(Math.max(1, page - 1), false)}
>
{t("actions.previous", { defaultValue: "上一页" })}
{t("actions.previous")}
</Button>
{visiblePages.map((p) => (
<Button
@@ -448,12 +448,12 @@ export function TicketOrdersListScreen() {
disabled={loading || page >= lastPage}
onClick={() => void fetchPage(Math.min(lastPage, page + 1), false)}
>
{t("actions.next", { defaultValue: "下一页" })}
{t("actions.next")}
</Button>
</div>
) : (
<p className="py-2 text-center text-xs text-slate-400">
{t("orders.noMore", { defaultValue: "没有更多注单" })}
{t("orders.noMore")}
</p>
)}
</>