refactor: enhance wallet transfer dialogs and forms

- Removed unused trigger styles and refactored button variants for improved consistency.
- Introduced new components for transfer information, previews, and error handling to streamline the UI.
- Updated layout and styling for better user experience in transfer dialogs and forms.
This commit is contained in:
2026-05-21 16:33:32 +08:00
parent 6b18e25766
commit 496ed10981
2 changed files with 138 additions and 135 deletions

View File

@@ -25,15 +25,6 @@ type BaseProps = {
idPrefix?: string;
};
const defaultInTrigger =
"!bg-[#0b3f96] !text-white shadow-[0_8px_18px_rgba(11,63,150,0.18)] hover:!bg-[#08357f]";
const defaultOutTrigger = "flex-1";
const hallInTrigger =
"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-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,
lotteryMinor,
@@ -52,31 +43,29 @@ export function TransferInDialog({
const { t } = useTranslation("player");
const resolvedTriggerLabel = triggerLabel ?? t("wallet.transferIn");
const triggerCombined = cn(
"inline-flex h-10 min-h-10 w-full min-w-0 flex-1 items-center justify-center gap-1.5 px-3 text-sm font-medium",
triggerVariant === "hall" ? hallInTrigger : defaultInTrigger,
triggerClassName,
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<Button
type="button"
variant="ghost"
className={triggerCombined}
variant="default"
className={cn(
"inline-flex h-10 min-h-10 w-full min-w-0 flex-1 items-center justify-center gap-1.5 px-3 text-sm font-medium",
triggerVariant === "hall" && "bg-[#07459f] text-white hover:bg-[#063b88]",
triggerClassName,
)}
onClick={() => setOpen(true)}
>
<ArrowDownLeft className="size-4 shrink-0" />
{resolvedTriggerLabel}
</Button>
<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">
<DialogContent showCloseButton className="gap-0 overflow-hidden p-0 sm:max-w-md">
<DialogHeader className="space-y-1.5 border-b border-border px-5 py-4 text-left">
<DialogTitle className="text-lg font-semibold">{t("wallet.transferInTitle")}</DialogTitle>
<DialogDescription className="text-sm leading-relaxed">
{t("wallet.dialogInDescription", { currency })}
</DialogDescription>
</DialogHeader>
<div className="px-5 pb-5 pt-4">
<div className="px-5 py-4">
<TransferInPanel
variant="dialog"
currency={currency}
@@ -112,36 +101,29 @@ export function TransferOutDialog({
const { t } = useTranslation("player");
const resolvedTriggerLabel = triggerLabel ?? t("wallet.transferOut");
const triggerCombined = cn(
"inline-flex h-10 min-h-10 w-full min-w-0 flex-1 items-center justify-center gap-1.5 px-3 text-sm font-medium",
triggerVariant === "hall"
? hallOutTrigger
: cn(
"!border-2 !border-input !bg-secondary !text-secondary-foreground hover:!bg-secondary/80",
defaultOutTrigger,
),
triggerClassName,
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<Button
type="button"
variant="ghost"
className={triggerCombined}
variant="outline"
className={cn(
"inline-flex h-10 min-h-10 w-full min-w-0 flex-1 items-center justify-center gap-1.5 px-3 text-sm font-medium",
triggerVariant === "hall" && "border-border bg-card hover:bg-muted",
triggerClassName,
)}
onClick={() => setOpen(true)}
>
<ArrowUpRight className="size-4 shrink-0" />
{resolvedTriggerLabel}
</Button>
<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">
<DialogContent showCloseButton className="gap-0 overflow-hidden p-0 sm:max-w-md">
<DialogHeader className="space-y-1.5 border-b border-border px-5 py-4 text-left">
<DialogTitle className="text-lg font-semibold">{t("wallet.transferOutTitle")}</DialogTitle>
<DialogDescription className="text-sm leading-relaxed">
{t("wallet.dialogOutDescription")}
</DialogDescription>
</DialogHeader>
<div className="px-5 pb-5 pt-4">
<div className="px-5 py-4">
<TransferOutPanel
variant="dialog"
currency={currency}

View File

@@ -2,7 +2,7 @@
import { isAxiosError } from "axios";
import { Loader2 } from "lucide-react";
import { useMemo, useState } from "react";
import { useMemo, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -55,6 +55,73 @@ type PanelBase = {
type PanelVariant = "dialog" | "page";
function TransferInfoBlock({ children }: { children: ReactNode }) {
return (
<div className="rounded-lg border border-border bg-muted/40 px-4 py-3 text-sm leading-relaxed">
{children}
</div>
);
}
function TransferPreview({ children }: { children: ReactNode }) {
return (
<p className="rounded-lg bg-muted/50 px-3 py-2 text-sm text-muted-foreground">{children}</p>
);
}
function TransferError({ message }: { message: string }) {
return (
<p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{message}
</p>
);
}
function TransferDialogFooter({
submitting,
confirmLabel,
onConfirm,
onCancel,
}: {
submitting: boolean;
confirmLabel: string;
onConfirm: () => void;
onCancel: () => void;
}) {
const { t } = useTranslation("player");
return (
<div className="mt-5 grid gap-2">
<Button
type="button"
size="lg"
className="h-11 w-full bg-[#07459f] text-base font-semibold text-white hover:bg-[#063b88]"
disabled={submitting}
onClick={onConfirm}
>
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
confirmLabel
)}
</Button>
<Button
type="button"
variant="outline"
size="lg"
className="h-10 w-full"
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
</div>
);
}
/** 弹窗内:取消关闭;独立页:仅展示提交(返回用顶栏或上方链接) */
export function TransferInPanel({
currency,
@@ -113,7 +180,8 @@ export function TransferInPanel({
variant === "page" ? (
<Button
type="button"
className="h-11 w-full rounded-lg bg-[#07459f] text-base font-bold text-white hover:bg-[#063b88]"
size="lg"
className="mt-5 h-11 w-full bg-[#07459f] text-base font-semibold text-white hover:bg-[#063b88]"
disabled={submitting}
onClick={() => void submit()}
>
@@ -127,51 +195,31 @@ export function TransferInPanel({
)}
</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()}
>
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
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>
<TransferDialogFooter
submitting={submitting}
confirmLabel={t("wallet.confirmIn")}
onConfirm={() => void submit()}
onCancel={onCancel}
/>
);
return (
<>
<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">
<div className="grid gap-4">
<TransferInfoBlock>
<p className="text-muted-foreground">
{t("wallet.mainBalance")}{" "}
<span className="font-semibold text-slate-700">{t("wallet.mainPending")}</span>
<span className="font-medium text-foreground">{t("wallet.mainPending")}</span>
</p>
<p className="mt-2 text-slate-500">
<p className="mt-2 text-muted-foreground">
{t("wallet.lotteryBalance")}{" "}
<span className="font-black tabular-nums text-[#0b3f96]">
<span className="font-semibold tabular-nums text-foreground">
{formatMinorAsCurrency(lotteryMinor, currency)}
</span>
</p>
</div>
<div className="grid gap-2.5">
<Label htmlFor={tid} className="text-sm font-black text-[#101a33]">{t("wallet.inAmount")}</Label>
</TransferInfoBlock>
<div className="grid gap-2">
<Label htmlFor={tid}>{t("wallet.inAmount")}</Label>
<Input
id={tid}
inputMode="decimal"
@@ -180,17 +228,15 @@ export function TransferInPanel({
onChange={(ev) => setAmountText(ev.target.value)}
disabled={submitting}
autoComplete="off"
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"
className="h-11 text-base tabular-nums"
/>
<p className="rounded-2xl bg-[#f8fbff] px-3 py-2 text-xs font-semibold leading-5 text-[#32518d]">
<TransferPreview>
{t("wallet.afterInPreview", {
amount: formatMinorAsCurrency(previewAfter, currency),
})}
</p>
</TransferPreview>
</div>
{localError ? (
<p className="rounded-2xl border border-red-200 bg-red-50 px-3 py-2 text-sm font-semibold text-[#d81435]">{localError}</p>
) : null}
{localError ? <TransferError message={localError} /> : null}
</div>
{footer}
</>
@@ -266,7 +312,8 @@ export function TransferOutPanel({
variant === "page" ? (
<Button
type="button"
className="h-11 w-full rounded-lg bg-[#e5002c] text-base font-bold text-white hover:bg-[#d10028]"
size="lg"
className="mt-5 h-11 w-full bg-[#07459f] text-base font-semibold text-white hover:bg-[#063b88]"
disabled={submitting}
onClick={() => void submit()}
>
@@ -280,52 +327,32 @@ export function TransferOutPanel({
)}
</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()}
>
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
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>
<TransferDialogFooter
submitting={submitting}
confirmLabel={t("wallet.confirmOut")}
onConfirm={() => void submit()}
onCancel={onCancel}
/>
);
return (
<>
<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">
<div className="grid gap-4">
<TransferInfoBlock>
<p className="text-muted-foreground">
{t("wallet.lotteryAvailable")}{" "}
<span className="font-black tabular-nums text-[#d81435]">
<span className="font-semibold tabular-nums text-foreground">
{formatMinorAsCurrency(availableMinor, currency)}
</span>
</p>
</div>
<div className="grid gap-2.5">
<div className="flex items-end justify-between gap-2">
<Label htmlFor={tid} className="text-sm font-black text-[#101a33]">{t("wallet.outAmount")}</Label>
</TransferInfoBlock>
<div className="grid gap-2">
<div className="flex items-center justify-between gap-2">
<Label htmlFor={tid}>{t("wallet.outAmount")}</Label>
<Button
type="button"
variant="link"
className="h-auto p-0 text-xs font-black text-[#d81435] hover:text-[#b80f2b]"
className="h-auto p-0 text-sm font-medium"
onClick={fillAll}
disabled={submitting || availableMinor < 1}
>
@@ -342,17 +369,15 @@ export function TransferOutPanel({
onChange={(ev) => setAmountText(ev.target.value)}
disabled={submitting}
autoComplete="off"
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"
className="h-11 text-base tabular-nums"
/>
<p className="rounded-2xl bg-[#fff7f8] px-3 py-2 text-xs font-semibold leading-5 text-[#9f1730]">
<TransferPreview>
{t("wallet.afterOutPreview", {
amount: formatMinorAsCurrency(previewAfter, currency),
})}
</p>
</TransferPreview>
</div>
{localError ? (
<p className="rounded-2xl border border-red-200 bg-red-50 px-3 py-2 text-sm font-semibold text-[#d81435]">{localError}</p>
) : null}
{localError ? <TransferError message={localError} /> : null}
</div>
{footer}
</>
@@ -375,12 +400,10 @@ export function TransferInPage({
backHref="/wallet"
backLabel={t("wallet.title")}
>
<Card className="rounded-xl border-[#e5edf8] shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
<Card>
<CardHeader>
<CardTitle className="text-[#0b3f96]">{t("wallet.transferInTitle")}</CardTitle>
<CardDescription>
{t("wallet.transferInDescription")}
</CardDescription>
<CardTitle>{t("wallet.transferInTitle")}</CardTitle>
<CardDescription>{t("wallet.transferInDescription")}</CardDescription>
</CardHeader>
<CardContent>
<TransferInPanel
@@ -413,12 +436,10 @@ export function TransferOutPage({
backHref="/wallet"
backLabel={t("wallet.title")}
>
<Card className="rounded-xl border-[#e5edf8] shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
<Card>
<CardHeader>
<CardTitle className="text-[#0b3f96]">{t("wallet.transferOutTitle")}</CardTitle>
<CardDescription>
{t("wallet.transferOutDescription")}
</CardDescription>
<CardTitle>{t("wallet.transferOutTitle")}</CardTitle>
<CardDescription>{t("wallet.transferOutDescription")}</CardDescription>
</CardHeader>
<CardContent>
<TransferOutPanel