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

@@ -152,12 +152,12 @@ export function WalletLogsBlock({
onClick={onLoadMore}
>
{loadingMore
? t("actions.loading", { defaultValue: "加载中..." })
: t("actions.loadMore", { defaultValue: "加载更多" })}
? t("actions.loading")
: t("actions.loadMore")}
</Button>
) : (
<p className="py-2 text-center text-xs text-slate-400">
{t("wallet.noMoreLogs", { defaultValue: "没有更多流水" })}
{t("wallet.noMoreLogs")}
</p>
)}
</>
@@ -179,29 +179,50 @@ export function LogRow({
const { t } = useTranslation("player");
const ccy = item.currency_code || currency;
const isIn = item.direction === "in";
const statusTone = item.status === "posted"
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: item.status === "pending_reconcile"
? "border-amber-200 bg-amber-50 text-amber-700"
: item.status === "reversed"
? "border-slate-200 bg-slate-50 text-slate-600"
: "border-blue-200 bg-blue-50 text-blue-700";
return (
<li className="rounded-xl border border-[#e5edf8] bg-white px-3 py-3 text-sm shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
<div className="flex items-start justify-between gap-2">
<div>
<span className="font-medium">
{logTypeLabel(item.type, t)}{" "}
<span className={isIn ? "font-black text-emerald-600" : "font-black text-[#0b3f96]"}>
{isIn ? "+" : ""}
{formatMinorAsCurrency(item.amount_abs, ccy)}
</span>
</span>
<p className="mt-1 text-xs text-muted-foreground">
{formatLocalDateTime(item.created_at)}{" "}
<span className="text-foreground/80">
· {txnStatusLabel(item.status, t)}
</span>
<li className="rounded-2xl border border-[#e1eaf6] bg-white px-4 py-3.5 text-sm shadow-[0_10px_28px_rgba(15,23,42,0.06)]">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={isIn ? "size-2.5 rounded-full bg-emerald-500" : "size-2.5 rounded-full bg-[#0b3f96]"} aria-hidden />
<p className="truncate text-base font-black leading-tight text-[#101a33]">
{logTypeLabel(item.type, t)}
</p>
</div>
<p className="mt-1.5 text-xs font-medium text-slate-500">
{formatLocalDateTime(item.created_at)}
</p>
{item.ref_id ? (
<p className="mt-0.5 font-mono text-[11px] text-muted-foreground">
<p className="mt-1 truncate font-mono text-[11px] text-slate-400">
{item.ref_id}
</p>
) : null}
</div>
<div className="flex shrink-0 flex-col items-end gap-2 text-right">
<p className={isIn ? "text-lg font-black tabular-nums text-emerald-600" : "text-lg font-black tabular-nums text-[#0b3f96]"}>
{isIn ? "+" : ""}
{formatMinorAsCurrency(item.amount_abs, ccy)}
</p>
<span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-black ${statusTone}`}>
{txnStatusLabel(item.status, t)}
</span>
</div>
</div>
<div className="mt-3 flex items-center justify-between rounded-xl bg-[#f8fbff] px-3 py-2 text-xs">
<span className="font-semibold text-slate-500">{t("wallet.balanceAfter")}</span>
<span className="font-mono font-black tabular-nums text-[#32518d]">
{formatMinorAsCurrency(item.balance_after, ccy)}
</span>
</div>
</li>
);

View File

@@ -194,8 +194,8 @@ export function WalletScreen() {
lotteryMinor={Number(balance?.balance ?? 0)}
onSuccess={refreshAll}
triggerVariant="hall"
triggerLabel={t("wallet.transferIn")}
triggerClassName="h-12 rounded-lg text-base font-bold"
triggerLabel={t("wallet.transferIn", { defaultValue: "Transfer In" })}
triggerClassName="h-14 rounded-2xl text-base font-black"
/>
<TransferOutDialog
idPrefix="wallet-"
@@ -203,8 +203,8 @@ export function WalletScreen() {
availableMinor={Number(balance?.available_balance ?? 0)}
onSuccess={refreshAll}
triggerVariant="hall"
triggerLabel={t("wallet.transferOut")}
triggerClassName="h-12 rounded-lg text-base font-bold"
triggerLabel={t("wallet.transferOut", { defaultValue: "Transfer Out" })}
triggerClassName="h-14 rounded-2xl text-base font-black"
/>
</div>

View File

@@ -26,15 +26,13 @@ type BaseProps = {
};
const defaultInTrigger =
"!bg-[#52c41a] !text-white shadow-none hover:!bg-[#52c41a]/90";
"!bg-[#0b3f96] !text-white shadow-[0_8px_18px_rgba(11,63,150,0.18)] hover:!bg-[#08357f]";
const defaultOutTrigger = "flex-1";
/** 高保真稿:蓝底白字 */
const hallInTrigger =
"rounded-lg border-0 !bg-[#1677ff] !text-white shadow-sm hover:!bg-[#1677ff]/90";
/** 高保真稿:白底红框红字 */
"rounded-2xl border border-[#d7e5fb] !bg-[#0b3f96] !text-white shadow-[0_10px_24px_rgba(11,63,150,0.22)] hover:!bg-[#08357f]";
const hallOutTrigger =
"rounded-lg !border-2 !border-[#ff4d4f] !bg-white !text-[#ff4d4f] shadow-sm hover:!bg-red-50 dark:!bg-card dark:hover:!bg-red-950/30";
"rounded-2xl border border-[#ffc7d2] !bg-white !text-[#e5002c] shadow-[0_8px_22px_rgba(216,20,53,0.08)] hover:!bg-[#fff5f7]";
export function TransferInDialog({
currency,
@@ -71,24 +69,26 @@ export function TransferInDialog({
<ArrowDownLeft className="size-4 shrink-0" />
{resolvedTriggerLabel}
</Button>
<DialogContent showCloseButton>
<DialogHeader>
<DialogTitle>{t("wallet.transferInTitle")}</DialogTitle>
<DialogDescription>
<DialogContent showCloseButton className="overflow-hidden rounded-[28px] border border-[#dbe7fb] bg-white p-0 shadow-[0_24px_70px_rgba(11,63,150,0.18)] sm:max-w-[430px] [&_[data-slot=dialog-close]]:right-3 [&_[data-slot=dialog-close]]:top-3 [&_[data-slot=dialog-close]]:text-white [&_[data-slot=dialog-close]]:hover:bg-white/15 [&_[data-slot=dialog-close]]:hover:text-white">
<DialogHeader className="bg-gradient-to-br from-[#0b3f96] via-[#1456bd] to-[#e5002c] px-5 pb-5 pt-6 text-white">
<DialogTitle className="text-xl font-black text-white">{t("wallet.transferInTitle")}</DialogTitle>
<DialogDescription className="text-sm leading-6 text-white/85">
{t("wallet.dialogInDescription", { currency })}
</DialogDescription>
</DialogHeader>
<TransferInPanel
variant="dialog"
currency={currency}
lotteryMinor={lotteryMinor}
idPrefix={idPrefix}
onCancel={() => setOpen(false)}
onSuccess={async () => {
await onSuccess();
setOpen(false);
}}
/>
<div className="px-5 pb-5 pt-4">
<TransferInPanel
variant="dialog"
currency={currency}
lotteryMinor={lotteryMinor}
idPrefix={idPrefix}
onCancel={() => setOpen(false)}
onSuccess={async () => {
await onSuccess();
setOpen(false);
}}
/>
</div>
</DialogContent>
</Dialog>
);
@@ -134,24 +134,26 @@ export function TransferOutDialog({
<ArrowUpRight className="size-4 shrink-0" />
{resolvedTriggerLabel}
</Button>
<DialogContent showCloseButton>
<DialogHeader>
<DialogTitle>{t("wallet.transferOutTitle")}</DialogTitle>
<DialogDescription>
<DialogContent showCloseButton className="overflow-hidden rounded-[28px] border border-[#ffd7df] bg-white p-0 shadow-[0_24px_70px_rgba(216,20,53,0.16)] sm:max-w-[430px] [&_[data-slot=dialog-close]]:right-3 [&_[data-slot=dialog-close]]:top-3 [&_[data-slot=dialog-close]]:text-white [&_[data-slot=dialog-close]]:hover:bg-white/15 [&_[data-slot=dialog-close]]:hover:text-white">
<DialogHeader className="bg-gradient-to-br from-[#e5002c] via-[#d81435] to-[#0b3f96] px-5 pb-5 pt-6 text-white">
<DialogTitle className="text-xl font-black text-white">{t("wallet.transferOutTitle")}</DialogTitle>
<DialogDescription className="text-sm leading-6 text-white/85">
{t("wallet.dialogOutDescription")}
</DialogDescription>
</DialogHeader>
<TransferOutPanel
variant="dialog"
currency={currency}
availableMinor={availableMinor}
idPrefix={idPrefix}
onCancel={() => setOpen(false)}
onSuccess={async () => {
await onSuccess();
setOpen(false);
}}
/>
<div className="px-5 pb-5 pt-4">
<TransferOutPanel
variant="dialog"
currency={currency}
availableMinor={availableMinor}
idPrefix={idPrefix}
onCancel={() => setOpen(false)}
onSuccess={async () => {
await onSuccess();
setOpen(false);
}}
/>
</div>
</DialogContent>
</Dialog>
);

View File

@@ -121,17 +121,10 @@ export function TransferInPanel({
)}
</Button>
) : (
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button
type="button"
variant="outline"
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
<div className="grid gap-2">
<Button
type="button"
className="h-12 w-full rounded-2xl bg-[#0b3f96] text-base font-black text-white shadow-[0_10px_22px_rgba(11,63,150,0.18)] hover:bg-[#08357f]"
disabled={submitting}
onClick={() => void submit()}
>
@@ -144,26 +137,35 @@ export function TransferInPanel({
t("wallet.confirmIn")
)}
</Button>
<Button
type="button"
variant="outline"
className="h-11 rounded-2xl border-[#dce7f7] bg-white text-base font-bold text-[#32518d] hover:bg-[#f8fbff]"
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
</div>
);
return (
<>
<div className="grid gap-3 py-1">
<div className="rounded-xl border border-[#e5edf8] bg-[#f8fbff] px-3 py-2 text-xs">
<p>
<div className="grid gap-4 py-1">
<div className="rounded-2xl border border-[#dbe7fb] bg-gradient-to-br from-[#f8fbff] to-white px-4 py-3 text-sm shadow-[0_8px_22px_rgba(11,63,150,0.06)]">
<p className="text-slate-500">
{t("wallet.mainBalance")}{" "}
<span className="text-muted-foreground">{t("wallet.mainPending")}</span>
<span className="font-semibold text-slate-700">{t("wallet.mainPending")}</span>
</p>
<p className="mt-1">
<p className="mt-2 text-slate-500">
{t("wallet.lotteryBalance")}{" "}
<span className="font-medium text-foreground">
<span className="font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(lotteryMinor, currency)}
</span>
</p>
</div>
<div className="grid gap-2">
<Label htmlFor={tid}>{t("wallet.inAmount")}</Label>
<div className="grid gap-2.5">
<Label htmlFor={tid} className="text-sm font-black text-[#101a33]">{t("wallet.inAmount")}</Label>
<Input
id={tid}
inputMode="decimal"
@@ -172,16 +174,16 @@ export function TransferInPanel({
onChange={(ev) => setAmountText(ev.target.value)}
disabled={submitting}
autoComplete="off"
className="h-11 rounded-lg border-[#dce7f7] bg-white text-base"
className="h-13 rounded-2xl border-[#d7e5fb] bg-white px-4 text-lg font-bold tabular-nums text-[#101a33] shadow-inner focus-visible:ring-[#0b3f96]/20"
/>
<p className="text-xs text-muted-foreground">
<p className="rounded-2xl bg-[#f8fbff] px-3 py-2 text-xs font-semibold leading-5 text-[#32518d]">
{t("wallet.afterInPreview", {
amount: formatMinorAsCurrency(previewAfter, currency),
})}
</p>
</div>
{localError ? (
<p className="text-sm text-[#ff4d4f]">{localError}</p>
<p className="rounded-2xl border border-red-200 bg-red-50 px-3 py-2 text-sm font-semibold text-[#d81435]">{localError}</p>
) : null}
</div>
{footer}
@@ -270,17 +272,10 @@ export function TransferOutPanel({
)}
</Button>
) : (
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button
type="button"
variant="outline"
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
<div className="grid gap-2">
<Button
type="button"
className="h-12 w-full rounded-2xl bg-[#e5002c] text-base font-black text-white shadow-[0_10px_22px_rgba(229,0,44,0.18)] hover:bg-[#d10028]"
disabled={submitting}
onClick={() => void submit()}
>
@@ -293,27 +288,36 @@ export function TransferOutPanel({
t("wallet.confirmOut")
)}
</Button>
<Button
type="button"
variant="outline"
className="h-11 rounded-2xl border-[#ffd7df] bg-white text-base font-bold text-[#d81435] hover:bg-[#fff5f7]"
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
</div>
);
return (
<>
<div className="grid gap-3 py-1">
<div className="rounded-xl border border-[#e5edf8] bg-[#f8fbff] px-3 py-2 text-xs">
<p>
<div className="grid gap-4 py-1">
<div className="rounded-2xl border border-[#ffd7df] bg-gradient-to-br from-[#fff7f8] to-white px-4 py-3 text-sm shadow-[0_8px_22px_rgba(216,20,53,0.06)]">
<p className="text-slate-500">
{t("wallet.lotteryAvailable")}{" "}
<span className="font-medium text-foreground">
<span className="font-black tabular-nums text-[#d81435]">
{formatMinorAsCurrency(availableMinor, currency)}
</span>
</p>
</div>
<div className="grid gap-2">
<div className="grid gap-2.5">
<div className="flex items-end justify-between gap-2">
<Label htmlFor={tid}>{t("wallet.outAmount")}</Label>
<Label htmlFor={tid} className="text-sm font-black text-[#101a33]">{t("wallet.outAmount")}</Label>
<Button
type="button"
variant="link"
className="h-auto p-0 text-xs"
className="h-auto p-0 text-xs font-black text-[#d81435] hover:text-[#b80f2b]"
onClick={fillAll}
disabled={submitting || availableMinor < 1}
>
@@ -330,16 +334,16 @@ export function TransferOutPanel({
onChange={(ev) => setAmountText(ev.target.value)}
disabled={submitting}
autoComplete="off"
className="h-11 rounded-lg border-[#dce7f7] bg-white text-base"
className="h-13 rounded-2xl border-[#ffd7df] bg-white px-4 text-lg font-bold tabular-nums text-[#101a33] shadow-inner focus-visible:ring-[#e5002c]/20"
/>
<p className="text-xs text-muted-foreground">
<p className="rounded-2xl bg-[#fff7f8] px-3 py-2 text-xs font-semibold leading-5 text-[#9f1730]">
{t("wallet.afterOutPreview", {
amount: formatMinorAsCurrency(previewAfter, currency),
})}
</p>
</div>
{localError ? (
<p className="text-sm text-[#ff4d4f]">{localError}</p>
<p className="rounded-2xl border border-red-200 bg-red-50 px-3 py-2 text-sm font-semibold text-[#d81435]">{localError}</p>
) : null}
</div>
{footer}