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

@@ -78,14 +78,14 @@ export function CheckWinningScreen() {
setResult(next);
setRecent((current) => [normalizedTicketNo, ...current.filter((x) => x !== normalizedTicketNo)].slice(0, 5));
} catch {
setError(t("results.check.loadFailed", { defaultValue: "查询失败,请稍后重试。" }));
setError(t("results.check.loadFailed"));
} finally {
setLoading(false);
}
}, [latestDraw, normalizedTicketNo, t]);
return (
<PlayerPanel title={t("results.check.title", { defaultValue: "查我的中奖" })} backHref="/results" backLabel={t("results.title")}>
<PlayerPanel title={t("results.check.title")} backHref="/results" backLabel={t("results.title")}>
<div className="space-y-4">
<section className="overflow-hidden rounded-2xl border border-red-100 bg-white shadow-[0_12px_32px_rgba(15,23,42,0.06)]">
<div className="bg-gradient-to-b from-red-50 to-white px-5 pb-5 pt-8 text-center">
@@ -93,28 +93,28 @@ export function CheckWinningScreen() {
<BriefcaseBusiness className="size-12" />
</div>
<h2 className="mt-5 text-lg font-black text-slate-950">
{t("results.check.enterTicket", { defaultValue: "输入你的票号或号码" })}
{t("results.check.enterTicket")}
</h2>
<p className="mx-auto mt-2 max-w-xs text-sm leading-relaxed text-slate-500">
{t("results.check.description", { defaultValue: "系统会按最新已发布期号查询你的注单和中奖情况。" })}
{t("results.check.description")}
</p>
</div>
<div className="space-y-3 px-4 pb-4">
<label className="block space-y-1.5">
<span className="text-xs font-black text-slate-700">
{t("results.check.ticketNumber", { defaultValue: "票号 / 号码" })}
{t("results.check.ticketNumber")}
</span>
<Input
value={ticketNo}
placeholder={t("results.check.placeholder", { defaultValue: "请输入票号或号码" })}
placeholder={t("results.check.placeholder")}
onChange={(e) => setTicketNo(e.target.value)}
className="h-12 rounded-xl border-[#dce7f7] bg-white font-mono text-base font-bold"
/>
</label>
{latestDraw ? (
<p className="text-xs text-slate-500">
{t("results.check.latestDraw", { drawNo: latestDraw.draw_no, defaultValue: "最新期号 {{drawNo}}" })}
{t("results.check.latestDraw", { drawNo: latestDraw.draw_no })}
</p>
) : null}
{error ? <p className="text-sm font-semibold text-[#e5002c]">{error}</p> : null}
@@ -124,7 +124,7 @@ export function CheckWinningScreen() {
onClick={() => void runCheck()}
className="h-12 w-full rounded-xl bg-[#e5002c] text-base font-black text-white hover:bg-[#d10028]"
>
{loading ? t("actions.loading", { defaultValue: "查询中..." }) : t("results.check.submit", { defaultValue: "立即查询" })}
{loading ? t("results.check.loading") : t("results.check.submit")}
</Button>
</div>
</section>
@@ -132,18 +132,18 @@ export function CheckWinningScreen() {
<section className="rounded-2xl border border-[#dfe8f6] bg-white p-4 shadow-[0_10px_26px_rgba(15,23,42,0.05)]">
<div className="flex items-center justify-between">
<h3 className="font-black text-slate-950">
{t("results.check.recent", { defaultValue: "最近查询" })}
{t("results.check.recent")}
</h3>
{recent.length > 0 ? (
<button type="button" className="text-sm font-bold text-[#0b56b7]" onClick={() => setRecent([])}>
{t("actions.clear", { defaultValue: "清空" })}
{t("actions.clear")}
</button>
) : null}
</div>
<div className="mt-3 divide-y divide-[#edf2f9]">
{recent.length === 0 ? (
<p className="py-4 text-sm text-slate-500">
{t("results.check.noRecent", { defaultValue: "暂无查询记录。" })}
{t("results.check.noRecent")}
</p>
) : (
recent.map((row) => (
@@ -213,7 +213,7 @@ function WinningResultDialog({
type="button"
onClick={() => onOpenChange(false)}
className="absolute right-3 top-3 z-10 inline-flex size-9 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
aria-label={t("actions.close", { defaultValue: "关闭" })}
aria-label={t("actions.close")}
>
<XIcon className="size-5" />
</button>
@@ -224,11 +224,11 @@ function WinningResultDialog({
</div>
<DialogTitle className="mt-3 text-xl font-black text-slate-950">
{isWon
? t("results.check.winTitle", { defaultValue: "恭喜,你中奖了" })
: t("results.check.noWinTitle", { defaultValue: "未查询到中奖" })}
? t("results.check.winTitle")
: t("results.check.noWinTitle")}
</DialogTitle>
<DialogDescription className="text-sm text-slate-500">
{t("results.check.ticketNumber", { defaultValue: "票号 / 号码" })}
{t("results.check.ticketNumber")}
</DialogDescription>
</DialogHeader>
@@ -239,15 +239,15 @@ function WinningResultDialog({
<div className="mt-5 grid grid-cols-2 overflow-hidden rounded-xl border border-emerald-100 bg-emerald-50 text-center">
<div className="border-r border-emerald-100 px-3 py-4">
<p className="text-xs font-medium text-slate-500">
{t("results.check.match", { defaultValue: "匹配" })}
{t("results.check.match")}
</p>
<p className="mt-2 text-lg font-black text-[#0a8f3e]">
{firstTicket ? playLabel(firstTicket.play_code, t) : isWon ? t("orders.hit", { defaultValue: "命中" }) : "—"}
{firstTicket ? playLabel(firstTicket.play_code, t) : isWon ? t("orders.hit") : "—"}
</p>
</div>
<div className="px-3 py-4">
<p className="text-xs font-medium text-slate-500">
{t("results.check.amount", { defaultValue: "中奖金额" })}
{t("results.check.amount")}
</p>
<p className="mt-2 font-mono text-lg font-black text-[#0a8f3e]">
{formatMinorAsCurrency(totalWin, firstTicket?.currency_code ?? "NPR")}
@@ -257,11 +257,11 @@ function WinningResultDialog({
<div className="mt-5 rounded-xl border border-[#e8eef7] bg-white p-4 text-sm">
<p className="font-black text-slate-950">
{t("results.check.drawInfo", { defaultValue: "开奖信息" })}
{t("results.check.drawInfo")}
</p>
<div className="mt-3 grid grid-cols-2 gap-4 text-slate-500">
<div>
<p className="text-xs">{t("results.check.issueNo", { defaultValue: "期号" })}</p>
<p className="text-xs">{t("results.check.issueNo")}</p>
<p className="mt-1 font-mono font-black text-slate-900">{data.draw.draw_no}</p>
</div>
<div>
@@ -283,7 +283,7 @@ function WinningResultDialog({
className="h-12 rounded-xl bg-[#07459f] text-base font-black text-white hover:bg-[#063b88]"
render={<Link href={`/orders?draw_no=${encodeURIComponent(data.draw.draw_no)}&number=${encodeURIComponent(query)}`} />}
>
{t("results.check.viewBetDetails", { defaultValue: "查看注单详情" })}
{t("results.check.viewBetDetails")}
</Button>
<Button
type="button"
@@ -292,7 +292,7 @@ function WinningResultDialog({
onClick={onCheckAnother}
>
<RefreshCw className="size-5" />
{t("results.check.checkAnother", { defaultValue: "查询另一张票" })}
{t("results.check.checkAnother")}
</Button>
</div>
</div>

View File

@@ -27,7 +27,7 @@ const RESULTS_PAGE_SIZE = 10;
const MONTH_OPTIONS = Array.from({ length: 12 }, (_, value) => ({
value,
label: `${value + 1}`,
labelKey: `calendar.months.${value + 1}`,
}));
export function DrawResultsListScreen() {
@@ -46,7 +46,6 @@ export function DrawResultsListScreen() {
const businessDate = /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined;
const quickYears = useMemo(() => buildYearOptions(calendarMonth), [calendarMonth]);
const featured = items?.[0] ?? null;
const olderDraw = items?.[1] ?? null;
const fetchList = useCallback(async (targetPage = 1, append = false) => {
setError(null);
@@ -121,7 +120,10 @@ export function DrawResultsListScreen() {
className="h-10 flex-1 justify-start rounded-lg border-[#dce7f7] bg-white px-3 text-left text-sm font-semibold text-[#32518d] hover:bg-[#f8fbff]"
>
<CalendarIcon className="mr-2 size-4 text-[#7890b8]" />
{date || t("results.selectBusinessDate", { defaultValue: "选择日期" })}
{date ||
t("results.selectBusinessDate", {
defaultValue: "选择日期",
})}
</Button>
}
/>
@@ -139,7 +141,7 @@ export function DrawResultsListScreen() {
<SelectContent className="max-h-64 min-w-24">
{MONTH_OPTIONS.map((month) => (
<SelectItem key={month.value} value={String(month.value)}>
{month.label}
{t(month.labelKey)}
</SelectItem>
))}
</SelectContent>
@@ -183,7 +185,7 @@ export function DrawResultsListScreen() {
size="icon"
variant="outline"
className="h-10 w-10 rounded-lg border-[#dce7f7] bg-white text-[#7890b8] hover:bg-[#f8fbff] hover:text-[#32518d]"
aria-label={t("actions.clear", { defaultValue: "清除" })}
aria-label={t("actions.clear")}
onClick={() => setDate("")}
>
<XIcon className="size-4" />
@@ -226,44 +228,28 @@ export function DrawResultsListScreen() {
<div className="space-y-3">
{featured ? (
<div className="rounded-xl border border-[#e5edf8] bg-white p-3 shadow-[0_10px_28px_rgba(15,23,42,0.06)]">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-[#edf2f9] pb-3">
<Button
type="button"
variant="outline"
size="sm"
className="min-w-[5rem] rounded-full border-[#e6edf8] bg-white text-slate-400"
disabled
>
{t("results.next")}
</Button>
<div className="min-w-0 flex-1 text-center">
<p className="font-mono text-lg font-black text-[#0b3f96]">
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-[#edf2f9] pb-3">
<div className="min-w-0">
<p className="text-[11px] font-black uppercase tracking-normal text-[#0b56b7]">
{t("results.detailTitle")}
</p>
<p className="mt-1 font-mono text-lg font-black text-[#0b3f96]">
{featured.draw_no}
</p>
<p className="mt-1 font-mono text-xs text-slate-500">
{t("results.drawTime", {
time: formatLotteryInstant(featured.draw_time_iso ?? featured.draw_time ?? null),
time: formatLotteryInstant(
featured.draw_time_iso ?? featured.draw_time ?? null,
),
})}
</p>
</div>
{olderDraw ? (
<Link
href={`/results/${encodeURIComponent(olderDraw.draw_no)}`}
className="inline-flex h-7 min-w-[5rem] items-center justify-center rounded-full border border-[#dce7f7] bg-white px-2.5 text-[0.8rem] font-medium text-[#0b56b7] transition-colors hover:bg-[#f1f6ff]"
>
{t("results.previous")}
</Link>
) : (
<Button
type="button"
variant="outline"
size="sm"
className="min-w-[5rem] rounded-full border-[#e6edf8] bg-white text-slate-400"
disabled
>
{t("results.previous")}
</Button>
)}
<Link
href={`/results/${encodeURIComponent(featured.draw_no)}`}
className="inline-flex h-8 shrink-0 items-center justify-center rounded-full border border-[#dce7f7] bg-white px-3 text-sm font-semibold text-[#0b56b7] transition-colors hover:bg-[#f1f6ff]"
>
{t("results.openDetail", { defaultValue: "查看详情" })}
</Link>
</div>
<div className="pt-4">
<TwentyThreeResultsGrid numbers={featured.results} />
@@ -323,12 +309,12 @@ export function DrawResultsListScreen() {
onClick={() => void fetchList(page + 1, true)}
>
{loadingMore
? t("actions.loading", { defaultValue: "加载中..." })
: t("actions.loadMore", { defaultValue: "加载更多" })}
? t("actions.loading")
: t("actions.loadMore")}
</Button>
) : items && items.length > 0 ? (
<p className="py-2 text-center text-xs text-slate-400">
{t("results.noMore", { defaultValue: "没有更多开奖结果" })}
{t("results.noMore")}
</p>
) : null}
</div>

View File

@@ -56,7 +56,7 @@ export function JackpotResultsStrip({
</p>
{gap !== null ? (
<p className="mt-1 text-xs font-semibold text-amber-800">
{t("results.jackpotGap", { count: gap, defaultValue: "距上次爆池 {{count}} 期" })}
{t("results.jackpotGap", { count: gap })}
</p>
) : null}
</div>