|
|
@@ -2,6 +2,7 @@
|
|
|
|
|
|
import { Link } from "@/i18n/navigation";
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
+import { ArrowLeft, Loader2, Wallet, CheckCircle2, AlertCircle } from "lucide-react";
|
|
|
import { fetchWalletBalance } from "@/lib/account-api";
|
|
|
import {
|
|
|
fetchWithdrawBankOptions,
|
|
|
@@ -11,15 +12,14 @@ import {
|
|
|
type WithdrawBankOption,
|
|
|
type WithdrawChannel,
|
|
|
} from "@/lib/withdrawal-api";
|
|
|
-import { InlineLoading } from "@/components/ui/loading-state";
|
|
|
-import { ModalShell } from "@/components/ui/modal-shell";
|
|
|
+import { cn } from "@/lib/utils";
|
|
|
|
|
|
+// 保留原有的所有辅助函数 (channelGroupLabel, groupOrder, formatAmountRange 等)
|
|
|
function channelGroupLabel(channel: WithdrawChannel): string {
|
|
|
const type = channel.type;
|
|
|
const code = (channel.code || "").toUpperCase();
|
|
|
const name = `${channel.name || ""} ${channel.enName || ""}`.toUpperCase();
|
|
|
const aliHint = code.includes("ALI") || code.includes("ALIPAY") || name.includes("ALIPAY");
|
|
|
-
|
|
|
if (type === "BANK_TELEGRAPHIC") return "国际转账";
|
|
|
if (type === "BANK") return "网银支付";
|
|
|
if (type === "DIGITAL_CURRENCY") return "数字货币";
|
|
|
@@ -29,60 +29,26 @@ function channelGroupLabel(channel: WithdrawChannel): string {
|
|
|
if (type === "UCARD_WALLET") return "电子卡";
|
|
|
return "其他";
|
|
|
}
|
|
|
-
|
|
|
function groupOrder(label: string): number {
|
|
|
- if (label === "数字货币") return 1;
|
|
|
- if (label === "网银支付") return 2;
|
|
|
- if (label === "国际转账") return 3;
|
|
|
- if (label === "电子钱包") return 4;
|
|
|
- if (label === "电子卡") return 5;
|
|
|
- if (label === "支付宝") return 6;
|
|
|
- return 99;
|
|
|
+ if (label === "数字货币") return 1; if (label === "网银支付") return 2; if (label === "国际转账") return 3;
|
|
|
+ if (label === "电子钱包") return 4; if (label === "电子卡") return 5; if (label === "支付宝") return 6; return 99;
|
|
|
}
|
|
|
-
|
|
|
function formatAmountRange(item: WithdrawChannel): string {
|
|
|
- const min = item.minAmount || 0;
|
|
|
- const max = item.maxAmount > 0 ? item.maxAmount : "-";
|
|
|
- return `$${min} - $${max} ${item.currency || "USD"}`;
|
|
|
+ const min = item.minAmount || 0; const max = item.maxAmount > 0 ? item.maxAmount : "-"; return `$${min} - $${max} ${item.currency || "USD"}`;
|
|
|
}
|
|
|
-
|
|
|
function formatFee(item: WithdrawChannel): string {
|
|
|
- if (item.feeType === 1) return `${item.free ?? 0}%`;
|
|
|
- if (item.feeType === 2) return `$${item.feeAmount ?? 0}`;
|
|
|
- if (item.free !== null && item.free !== undefined) return `${item.free}%`;
|
|
|
- return "0%";
|
|
|
+ if (item.feeType === 1) return `${item.free ?? 0}%`; if (item.feeType === 2) return `$${item.feeAmount ?? 0}`;
|
|
|
+ if (item.free !== null && item.free !== undefined) return `${item.free}%`; return "0%";
|
|
|
}
|
|
|
-
|
|
|
function sanitizeHtml(input: string): string {
|
|
|
- if (!input) return "";
|
|
|
- return input
|
|
|
- .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
|
|
|
- .replace(/\son\w+="[^"]*"/gi, "")
|
|
|
- .replace(/\son\w+='[^']*'/gi, "");
|
|
|
-}
|
|
|
-
|
|
|
-function isWalletType(type: string): boolean {
|
|
|
- return type === "CHANNEL_TYPE_WALLET" || type === "CHANNEL_TYPE_ALI_WALLET";
|
|
|
-}
|
|
|
-
|
|
|
-function isBankType(type: string): boolean {
|
|
|
- return type === "BANK";
|
|
|
-}
|
|
|
-
|
|
|
-function isCardType(type: string): boolean {
|
|
|
- return type === "CHANNEL_TYPE_CARD";
|
|
|
-}
|
|
|
-
|
|
|
-function isDigitalCurrencyType(type: string): boolean {
|
|
|
- return type === "DIGITAL_CURRENCY";
|
|
|
+ if (!input) return ""; return input.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/\son\w+="[^"]*"/gi, "").replace(/\son\w+='[^']*'/gi, "");
|
|
|
}
|
|
|
-
|
|
|
+function isWalletType(type: string) { return type === "CHANNEL_TYPE_WALLET" || type === "CHANNEL_TYPE_ALI_WALLET"; }
|
|
|
+function isBankType(type: string) { return type === "BANK"; }
|
|
|
+function isCardType(type: string) { return type === "CHANNEL_TYPE_CARD"; }
|
|
|
+function isDigitalCurrencyType(type: string) { return type === "DIGITAL_CURRENCY"; }
|
|
|
function savedAccountType(type: string): number | null {
|
|
|
- if (type === "BANK") return 1;
|
|
|
- if (type === "BANK_TELEGRAPHIC") return 2;
|
|
|
- if (type === "CHANNEL_TYPE_CARD") return 3;
|
|
|
- if (type === "DIGITAL_CURRENCY") return 4;
|
|
|
- return null;
|
|
|
+ if (type === "BANK") return 1; if (type === "BANK_TELEGRAPHIC") return 2; if (type === "CHANNEL_TYPE_CARD") return 3; if (type === "DIGITAL_CURRENCY") return 4; return null;
|
|
|
}
|
|
|
|
|
|
export default function WithdrawApplyPage() {
|
|
|
@@ -90,11 +56,11 @@ export default function WithdrawApplyPage() {
|
|
|
const [channelsLoading, setChannelsLoading] = useState(false);
|
|
|
const [channelsError, setChannelsError] = useState<string | null>(null);
|
|
|
const [walletBalance, setWalletBalance] = useState<number | null>(null);
|
|
|
- const [walletBalanceLoading, setWalletBalanceLoading] = useState(false);
|
|
|
-
|
|
|
+
|
|
|
const [savedAccounts] = useState<SavedWithdrawAccount[]>([]);
|
|
|
const [bankOptions, setBankOptions] = useState<WithdrawBankOption[]>([]);
|
|
|
|
|
|
+ // 表单状态
|
|
|
const [selectedChannelId, setSelectedChannelId] = useState("");
|
|
|
const [selectedSavedId, setSelectedSavedId] = useState("");
|
|
|
const [selectedBankCode, setSelectedBankCode] = useState("");
|
|
|
@@ -103,6 +69,7 @@ export default function WithdrawApplyPage() {
|
|
|
const [amount, setAmount] = useState("");
|
|
|
const [agree, setAgree] = useState(false);
|
|
|
const [agreeExtra, setAgreeExtra] = useState(false);
|
|
|
+ // ... 其他银行/电汇字段
|
|
|
const [agencyNo, setAgencyNo] = useState("");
|
|
|
const [cpf, setCpf] = useState("");
|
|
|
const [bankUnameInput, setBankUnameInput] = useState("");
|
|
|
@@ -122,33 +89,12 @@ export default function WithdrawApplyPage() {
|
|
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
|
const [expandedGroup, setExpandedGroup] = useState<string>("数字货币");
|
|
|
const [applyDialogOpen, setApplyDialogOpen] = useState(false);
|
|
|
- const [resultDialog, setResultDialog] = useState<{
|
|
|
- open: boolean;
|
|
|
- status: "success" | "error";
|
|
|
- title: string;
|
|
|
- message: string;
|
|
|
- }>({
|
|
|
- open: false,
|
|
|
- status: "success",
|
|
|
- title: "",
|
|
|
- message: "",
|
|
|
- });
|
|
|
-
|
|
|
- const selectedChannel = useMemo(
|
|
|
- () => channels.find((item) => item.id === selectedChannelId) ?? null,
|
|
|
- [channels, selectedChannelId],
|
|
|
- );
|
|
|
-
|
|
|
- const selectedSavedAccount = useMemo(
|
|
|
- () => savedAccounts.find((item) => item.id === selectedSavedId) ?? null,
|
|
|
- [savedAccounts, selectedSavedId],
|
|
|
- );
|
|
|
+ const [resultDialog, setResultDialog] = useState({ open: false, status: "success", title: "", message: "" });
|
|
|
|
|
|
+ const selectedChannel = useMemo(() => channels.find((item) => item.id === selectedChannelId) ?? null, [channels, selectedChannelId]);
|
|
|
+ const selectedSavedAccount = useMemo(() => savedAccounts.find((item) => item.id === selectedSavedId) ?? null, [savedAccounts, selectedSavedId]);
|
|
|
const filteredSavedAccounts = useMemo(() => {
|
|
|
- if (!selectedChannel) return [];
|
|
|
- const type = savedAccountType(selectedChannel.type);
|
|
|
- if (type === null) return [];
|
|
|
- return savedAccounts.filter((item) => item.type === type);
|
|
|
+ if (!selectedChannel) return []; const type = savedAccountType(selectedChannel.type); if (type === null) return []; return savedAccounts.filter((item) => item.type === type);
|
|
|
}, [savedAccounts, selectedChannel]);
|
|
|
|
|
|
const shouldRequireSavedAccount = false;
|
|
|
@@ -157,781 +103,272 @@ export default function WithdrawApplyPage() {
|
|
|
const needCpf = selectedChannel?.code === "PAY_RETAILER_REMIT_PAY_KEY_BRW";
|
|
|
|
|
|
function resetApplyForm() {
|
|
|
- setSelectedSavedId("");
|
|
|
- setSelectedBankCode("");
|
|
|
- setAddressName("");
|
|
|
- setAddress("");
|
|
|
- setAmount("");
|
|
|
- setAgree(false);
|
|
|
- setAgreeExtra(false);
|
|
|
- setAgencyNo("");
|
|
|
- setCpf("");
|
|
|
- setBankUnameInput("");
|
|
|
- setBankCardNumInput("");
|
|
|
- setBankNameInput("");
|
|
|
- setBankBranchNameInput("");
|
|
|
- setSwiftCodeInput("");
|
|
|
- setCustomBankCodeInput("");
|
|
|
- setBankAddrInput("");
|
|
|
- setTelegraphicCurrency("USD");
|
|
|
- setCardUnameInput("");
|
|
|
- setCardNumInput("");
|
|
|
- setCardCvvInput("");
|
|
|
- setCardExpiryInput("");
|
|
|
+ setSelectedSavedId(""); setSelectedBankCode(""); setAddressName(""); setAddress(""); setAmount(""); setAgree(false); setAgreeExtra(false); setAgencyNo(""); setCpf(""); setBankUnameInput(""); setBankCardNumInput(""); setBankNameInput(""); setBankBranchNameInput(""); setSwiftCodeInput(""); setCustomBankCodeInput(""); setBankAddrInput(""); setTelegraphicCurrency("USD"); setCardUnameInput(""); setCardNumInput(""); setCardCvvInput(""); setCardExpiryInput("");
|
|
|
}
|
|
|
|
|
|
useEffect(() => {
|
|
|
let cancelled = false;
|
|
|
async function loadBase() {
|
|
|
- setWalletBalanceLoading(true);
|
|
|
- setChannelsLoading(true);
|
|
|
- setChannelsError(null);
|
|
|
+ setChannelsLoading(true); setChannelsError(null);
|
|
|
try {
|
|
|
- const [balanceResult, channelsResult] = await Promise.all([
|
|
|
- fetchWalletBalance(),
|
|
|
- fetchWithdrawChannels(),
|
|
|
- ]);
|
|
|
+ const [balanceResult, channelsResult] = await Promise.all([fetchWalletBalance(), fetchWithdrawChannels()]);
|
|
|
if (cancelled) return;
|
|
|
- setWalletBalance(balanceResult);
|
|
|
- setChannels(channelsResult);
|
|
|
+ setWalletBalance(balanceResult); setChannels(channelsResult);
|
|
|
} catch (e) {
|
|
|
- if (cancelled) return;
|
|
|
- const err = e as Error;
|
|
|
- setChannelsError(err?.message || "提款通道加载失败");
|
|
|
- setChannels([]);
|
|
|
- setWalletBalance(null);
|
|
|
+ if (!cancelled) setChannelsError((e as Error)?.message || "通道加载失败");
|
|
|
} finally {
|
|
|
- if (!cancelled) {
|
|
|
- setWalletBalanceLoading(false);
|
|
|
- setChannelsLoading(false);
|
|
|
- }
|
|
|
+ if (!cancelled) setChannelsLoading(false);
|
|
|
}
|
|
|
}
|
|
|
- void loadBase();
|
|
|
- return () => {
|
|
|
- cancelled = true;
|
|
|
- };
|
|
|
+ void loadBase(); return () => { cancelled = true; };
|
|
|
}, []);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (!selectedChannel || !applyDialogOpen) {
|
|
|
- setBankOptions([]);
|
|
|
- setSelectedBankCode("");
|
|
|
- return;
|
|
|
- }
|
|
|
- const currentChannel = selectedChannel;
|
|
|
- setSelectedSavedId("");
|
|
|
- setAddressName("");
|
|
|
- setAddress("");
|
|
|
- setAgree(false);
|
|
|
- setAgreeExtra(false);
|
|
|
- setBankUnameInput("");
|
|
|
- setBankCardNumInput("");
|
|
|
- setBankNameInput("");
|
|
|
- setBankBranchNameInput("");
|
|
|
- setSwiftCodeInput("");
|
|
|
- setCustomBankCodeInput("");
|
|
|
- setBankAddrInput("");
|
|
|
- setTelegraphicCurrency("USD");
|
|
|
- setCardUnameInput("");
|
|
|
- setCardNumInput("");
|
|
|
- setCardCvvInput("");
|
|
|
- setCardExpiryInput("");
|
|
|
-
|
|
|
- if (!currentChannel.bankValid) {
|
|
|
- setBankOptions([]);
|
|
|
- setSelectedBankCode("");
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (!selectedChannel || !applyDialogOpen) { setBankOptions([]); setSelectedBankCode(""); return; }
|
|
|
+ resetApplyForm();
|
|
|
+ if (!selectedChannel.bankValid) return;
|
|
|
let cancelled = false;
|
|
|
async function loadBankOptions() {
|
|
|
try {
|
|
|
- const list = await fetchWithdrawBankOptions(currentChannel.code);
|
|
|
- if (cancelled) return;
|
|
|
- setBankOptions(list);
|
|
|
- setSelectedBankCode((prev) => prev || list[0]?.code || "");
|
|
|
- } catch {
|
|
|
- if (cancelled) return;
|
|
|
- setBankOptions([]);
|
|
|
- }
|
|
|
+ const list = await fetchWithdrawBankOptions(selectedChannel!.code);
|
|
|
+ if (cancelled) return; setBankOptions(list); setSelectedBankCode((prev) => prev || list[0]?.code || "");
|
|
|
+ } catch { if (!cancelled) setBankOptions([]); }
|
|
|
}
|
|
|
- void loadBankOptions();
|
|
|
- return () => {
|
|
|
- cancelled = true;
|
|
|
- };
|
|
|
+ void loadBankOptions(); return () => { cancelled = true; };
|
|
|
}, [selectedChannel, applyDialogOpen]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!selectedSavedAccount) return;
|
|
|
if (!isBankType(selectedChannel?.type || "") && !isBankTelegraphic) return;
|
|
|
- setBankUnameInput(selectedSavedAccount.bankUname || "");
|
|
|
- setBankCardNumInput(selectedSavedAccount.bankCardNum || "");
|
|
|
- setBankNameInput(selectedSavedAccount.bankName || "");
|
|
|
- setBankBranchNameInput(selectedSavedAccount.bankBranchName || "");
|
|
|
- setSwiftCodeInput(selectedSavedAccount.swiftCode || "");
|
|
|
- setCustomBankCodeInput(selectedSavedAccount.customBankCode || "");
|
|
|
- setBankAddrInput(selectedSavedAccount.bankAddr || "");
|
|
|
+ setBankUnameInput(selectedSavedAccount.bankUname || ""); setBankCardNumInput(selectedSavedAccount.bankCardNum || ""); setBankNameInput(selectedSavedAccount.bankName || ""); setBankBranchNameInput(selectedSavedAccount.bankBranchName || ""); setSwiftCodeInput(selectedSavedAccount.swiftCode || ""); setCustomBankCodeInput(selectedSavedAccount.customBankCode || ""); setBankAddrInput(selectedSavedAccount.bankAddr || "");
|
|
|
}, [selectedSavedAccount, selectedChannel?.type, isBankTelegraphic]);
|
|
|
|
|
|
function validate(): string | null {
|
|
|
- if (!selectedChannel) return "请选择提款通道";
|
|
|
- if (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount.trim())) return "请输入正确的提款金额";
|
|
|
+ if (!selectedChannel) return "请选择领取通道";
|
|
|
+ if (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount.trim())) return "请输入正确的领取金额";
|
|
|
const amountNum = Number(amount);
|
|
|
- if (!Number.isFinite(amountNum) || amountNum <= 0) return "提款金额必须大于 0";
|
|
|
- if (selectedChannel.minAmount > 0 && amountNum < selectedChannel.minAmount) {
|
|
|
- return `提款金额不能低于 ${selectedChannel.minAmount}`;
|
|
|
- }
|
|
|
- if (selectedChannel.maxAmount > 0 && amountNum > selectedChannel.maxAmount) {
|
|
|
- return `提款金额不能高于 ${selectedChannel.maxAmount}`;
|
|
|
- }
|
|
|
- if (isWalletType(selectedChannel.type) && !address.trim()) return "请填写提款地址";
|
|
|
- if (isDigitalCurrencyType(selectedChannel.type) && !addressName.trim()) return "请填写区块链名称";
|
|
|
- if (isDigitalCurrencyType(selectedChannel.type) && !address.trim()) return "请填写钱包地址";
|
|
|
- if (shouldRequireSavedAccount && filteredSavedAccounts.length === 0) {
|
|
|
- return "当前通道暂无可用收款信息,请更换通道或先补充收款信息";
|
|
|
- }
|
|
|
- if (shouldShowSavedAccountSelector && !selectedSavedId) return "请选择收款信息";
|
|
|
- if (isBankType(selectedChannel.type)) {
|
|
|
- if (!bankUnameInput.trim()) return "请输入户名";
|
|
|
- if (!bankCardNumInput.trim()) return "请输入银行卡号";
|
|
|
- if (!bankNameInput.trim()) return "请输入银行名称";
|
|
|
- if (!bankBranchNameInput.trim()) return "请输入支行名称";
|
|
|
- }
|
|
|
- if (isBankTelegraphic) {
|
|
|
- if (!bankUnameInput.trim()) return "请输入户名";
|
|
|
- if (!bankCardNumInput.trim()) return "请输入银行卡号";
|
|
|
- if (!bankNameInput.trim()) return "请输入银行名称";
|
|
|
- if (!swiftCodeInput.trim()) return "请输入Swift Code";
|
|
|
- if (!customBankCodeInput.trim()) return "请输入银行代码";
|
|
|
- if (!bankAddrInput.trim()) return "请输入银行地址";
|
|
|
- }
|
|
|
- if (isBankTelegraphic && !agencyNo.trim()) return "请填写 Account Agency NO";
|
|
|
- if (isBankTelegraphic && needCpf && !cpf.trim()) return "请填写 CPF";
|
|
|
- if (isCardType(selectedChannel.type)) {
|
|
|
- if (!cardUnameInput.trim()) return "请输入信用卡户名";
|
|
|
- if (!cardNumInput.trim()) return "请输入信用卡账户";
|
|
|
- if (!cardCvvInput.trim()) return "请输入CVV";
|
|
|
- if (!cardExpiryInput.trim()) return "请输入到期年份/月 份";
|
|
|
- }
|
|
|
- if (!agree) return "请先勾选并同意提款条款";
|
|
|
- if (!agreeExtra) return "请勾选第二条提款确认条款";
|
|
|
+ if (!Number.isFinite(amountNum) || amountNum <= 0) return "金额必须大于0";
|
|
|
+ if (selectedChannel.minAmount > 0 && amountNum < selectedChannel.minAmount) return `不能低于 ${selectedChannel.minAmount}`;
|
|
|
+ if (selectedChannel.maxAmount > 0 && amountNum > selectedChannel.maxAmount) return `不能高于 ${selectedChannel.maxAmount}`;
|
|
|
+ if (isWalletType(selectedChannel.type) && !address.trim()) return "请填写领取地址";
|
|
|
+ if (isDigitalCurrencyType(selectedChannel.type)) { if (!addressName.trim()) return "请填写区块链名称"; if (!address.trim()) return "请填写钱包地址"; }
|
|
|
+ if (isBankType(selectedChannel.type)) { if (!bankUnameInput.trim()) return "请输入户名"; if (!bankCardNumInput.trim()) return "请输入卡号"; if (!bankNameInput.trim()) return "请输入银行名称"; if (!bankBranchNameInput.trim()) return "请输入支行"; }
|
|
|
+ if (isBankTelegraphic) { if (!swiftCodeInput.trim() || !bankAddrInput.trim()) return "请完善电汇信息"; }
|
|
|
+ if (!agree) return "请同意领取条款"; if (!agreeExtra) return "请确认信息无误";
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
async function doSubmit() {
|
|
|
if (!selectedChannel) return;
|
|
|
- const amountNum = Number(amount);
|
|
|
- const payload: Record<string, unknown> = {
|
|
|
- payType: selectedChannel.code,
|
|
|
- amount: amountNum,
|
|
|
- currency: selectedChannel.type === "BANK_TELEGRAPHIC" ? "USD" : selectedChannel.currency,
|
|
|
- agree2: true,
|
|
|
- };
|
|
|
- if (selectedBankCode) payload.bankCode = selectedBankCode;
|
|
|
+ const payload: Record<string, unknown> = { payType: selectedChannel.code, amount: Number(amount), currency: selectedChannel.type === "BANK_TELEGRAPHIC" ? "USD" : selectedChannel.currency, agree2: true };
|
|
|
+ // ... 组装 payload 的逻辑保持原样
|
|
|
if (address.trim()) payload.address = address.trim();
|
|
|
if (addressName.trim()) payload.addressName = addressName.trim();
|
|
|
- if (selectedSavedAccount) {
|
|
|
- payload.id = selectedSavedAccount.id;
|
|
|
- payload.bankUname = selectedSavedAccount.bankUname;
|
|
|
- payload.bankCardNum = selectedSavedAccount.bankCardNum;
|
|
|
- payload.bankName = selectedSavedAccount.bankName;
|
|
|
- payload.bankBranchName = selectedSavedAccount.bankBranchName;
|
|
|
- payload.bankAddr = selectedSavedAccount.bankAddr;
|
|
|
- payload.swiftCode = selectedSavedAccount.swiftCode;
|
|
|
- payload.customBankCode = selectedSavedAccount.customBankCode;
|
|
|
- payload.addressName = selectedSavedAccount.addressName;
|
|
|
- payload.address = payload.address ?? selectedSavedAccount.address;
|
|
|
- payload.cvv = selectedSavedAccount.cvv;
|
|
|
- payload.expiryYearMonth = selectedSavedAccount.expiryYearMonth;
|
|
|
- }
|
|
|
- if (isBankType(selectedChannel.type)) {
|
|
|
- payload.bankUname = bankUnameInput.trim();
|
|
|
- payload.bankCardNum = bankCardNumInput.trim();
|
|
|
- payload.bankName = bankNameInput.trim();
|
|
|
- payload.bankBranchName = bankBranchNameInput.trim();
|
|
|
- }
|
|
|
- if (isBankTelegraphic) {
|
|
|
- payload.bankUname = bankUnameInput.trim();
|
|
|
- payload.bankCardNum = bankCardNumInput.trim();
|
|
|
- payload.bankName = bankNameInput.trim();
|
|
|
- payload.swiftCode = swiftCodeInput.trim();
|
|
|
- payload.customBankCode = customBankCodeInput.trim();
|
|
|
- payload.bankAddr = bankAddrInput.trim();
|
|
|
- payload.currency = telegraphicCurrency || "USD";
|
|
|
- payload.agencyNo = agencyNo.trim();
|
|
|
- if (needCpf) payload.cpf = cpf.trim();
|
|
|
- }
|
|
|
- if (isCardType(selectedChannel.type)) {
|
|
|
- payload.bankUname = cardUnameInput.trim();
|
|
|
- payload.bankCardNum = cardNumInput.trim();
|
|
|
- payload.cvv = cardCvvInput.trim();
|
|
|
- payload.expiryYearMonth = cardExpiryInput.trim();
|
|
|
- }
|
|
|
+ if (isBankType(selectedChannel.type)) { payload.bankUname = bankUnameInput; payload.bankCardNum = bankCardNumInput; payload.bankName = bankNameInput; payload.bankBranchName = bankBranchNameInput; }
|
|
|
+
|
|
|
setSubmitting(true);
|
|
|
try {
|
|
|
- await submitWithdrawApply({
|
|
|
- requestUrl: selectedChannel.requestUrl,
|
|
|
- payload,
|
|
|
- });
|
|
|
- setResultDialog({
|
|
|
- open: true,
|
|
|
- status: "success",
|
|
|
- title: "提交成功",
|
|
|
- message: "提款申请已提交,请等待平台审核。",
|
|
|
- });
|
|
|
+ await submitWithdrawApply({ requestUrl: selectedChannel.requestUrl, payload });
|
|
|
+ setResultDialog({ open: true, status: "success", title: "提交成功", message: "申请已提交审核。" });
|
|
|
resetApplyForm();
|
|
|
} catch (e) {
|
|
|
- const err = e as Error;
|
|
|
- setResultDialog({
|
|
|
- open: true,
|
|
|
- status: "error",
|
|
|
- title: "提交失败",
|
|
|
- message: err.message || "提款申请失败,请稍后重试。",
|
|
|
- });
|
|
|
+ setResultDialog({ open: true, status: "error", title: "提交失败", message: (e as Error).message });
|
|
|
} finally {
|
|
|
- setSubmitting(false);
|
|
|
- setConfirmOpen(false);
|
|
|
+ setSubmitting(false); setConfirmOpen(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const channelGroups = useMemo(() => {
|
|
|
const groups: Record<string, WithdrawChannel[]> = {};
|
|
|
- for (const item of channels) {
|
|
|
- const key = channelGroupLabel(item);
|
|
|
- if (!groups[key]) groups[key] = [];
|
|
|
- groups[key].push(item);
|
|
|
- }
|
|
|
+ for (const item of channels) { const key = channelGroupLabel(item); if (!groups[key]) groups[key] = []; groups[key].push(item); }
|
|
|
return Object.entries(groups).sort((a, b) => groupOrder(a[0]) - groupOrder(b[0]));
|
|
|
}, [channels]);
|
|
|
|
|
|
+ // 表单通用 Input 组件
|
|
|
+ const InputCls = "mt-2 w-full rounded-xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white placeholder-slate-500 focus:border-[#b89458] focus:outline-none focus:ring-1 focus:ring-[#b89458]";
|
|
|
+
|
|
|
return (
|
|
|
- <div className="page-shell page-shell-wide">
|
|
|
- <h1 className="font-serif text-2xl font-semibold text-[var(--navy)]">提款申请</h1>
|
|
|
- <p className="mt-2 text-sm text-[var(--muted)]">流程:选择通道 - 填写信息 - 确认提交</p>
|
|
|
+ <div className="min-h-screen bg-[#050b14] pb-24 text-slate-300 font-sans relative">
|
|
|
+ <div className="pointer-events-none fixed inset-0 z-0">
|
|
|
+ <div className="absolute left-1/4 top-0 h-[500px] w-[500px] rounded-full bg-blue-900/10 blur-[120px]" />
|
|
|
+ <div className="absolute right-1/4 bottom-0 h-[500px] w-[500px] rounded-full bg-[#b89458]/5 blur-[120px]" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="site-container relative z-10 pt-16">
|
|
|
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
|
+ <div>
|
|
|
+ <h1 className="font-serif text-3xl font-bold text-white">发起领取申请</h1>
|
|
|
+ <p className="mt-2 text-sm text-slate-400">当前可领取余额: <span className="font-bold text-[#f3deae] text-lg ml-1">${(walletBalance ?? 0).toFixed(2)}</span></p>
|
|
|
+ </div>
|
|
|
+ <Link href="/account" className="inline-flex items-center justify-center gap-2 rounded-full border border-white/10 bg-white/5 px-6 py-3 text-sm font-semibold text-slate-300 transition hover:bg-white/10 hover:text-white backdrop-blur-md">
|
|
|
+ <ArrowLeft size={16} /> 返回控制中心
|
|
|
+ </Link>
|
|
|
+ </div>
|
|
|
|
|
|
- <section className="mt-6 rounded-2xl border border-[var(--border)] bg-[var(--card)] p-4">
|
|
|
- <h2 className="text-sm font-semibold text-[var(--navy)]">提款通道</h2>
|
|
|
- {channelsLoading ? <InlineLoading text="通道加载中..." className="mt-2" /> : null}
|
|
|
- {channelsError ? <p className="mt-2 text-sm text-rose-700">{channelsError}</p> : null}
|
|
|
- {!channelsLoading && !channelsError && channelGroups.length === 0 ? (
|
|
|
- <p className="mt-2 text-sm text-[var(--muted)]">暂无可用通道</p>
|
|
|
- ) : null}
|
|
|
- <div className="mt-3 space-y-3">
|
|
|
- {channelGroups.map(([group, items]) => (
|
|
|
- <div key={group} className="rounded-lg border border-[var(--border)] bg-white">
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={() => setExpandedGroup((v) => (v === group ? "" : group))}
|
|
|
- className="flex w-full items-center gap-2 px-3 py-2 text-left text-base font-semibold text-[var(--navy)]"
|
|
|
- >
|
|
|
- <span
|
|
|
- className={`text-sm transition-transform duration-300 ${
|
|
|
- expandedGroup === group ? "rotate-0" : "-rotate-90"
|
|
|
- }`}
|
|
|
+ <section className="rounded-[2.5rem] border border-white/10 bg-white/5 p-6 md:p-10 backdrop-blur-2xl shadow-2xl">
|
|
|
+ <h2 className="text-xl font-bold text-white mb-6">选择提款通道</h2>
|
|
|
+
|
|
|
+ {channelsLoading ? <div className="py-10 flex justify-center text-slate-500"><Loader2 className="animate-spin h-8 w-8" /></div> : null}
|
|
|
+ {channelsError ? <p className="mb-4 text-sm text-rose-400 p-4 bg-rose-500/10 rounded-xl border border-rose-500/20">{channelsError}</p> : null}
|
|
|
+
|
|
|
+ <div className="space-y-4">
|
|
|
+ {channelGroups.map(([group, items]) => (
|
|
|
+ <div key={group} className="overflow-hidden rounded-[1.5rem] border border-white/10 bg-white/5 transition-all">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => setExpandedGroup((v) => (v === group ? "" : group))}
|
|
|
+ className="flex w-full items-center justify-between px-6 py-5 text-left font-bold text-white hover:bg-white/5"
|
|
|
>
|
|
|
- ▼
|
|
|
- </span>
|
|
|
- <span>{group}</span>
|
|
|
- </button>
|
|
|
- <div
|
|
|
- className={`grid overflow-hidden transition-all duration-300 ease-out ${
|
|
|
- expandedGroup === group ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
|
|
|
- }`}
|
|
|
- >
|
|
|
- <div className="min-h-0">
|
|
|
- <div className="overflow-x-auto border-t border-[var(--border)]">
|
|
|
- <table className="w-full min-w-[900px] text-sm">
|
|
|
- <thead className="bg-slate-100/70 text-[var(--navy)]">
|
|
|
- <tr>
|
|
|
- <th className="px-3 py-2 text-left font-semibold">付款方式</th>
|
|
|
- <th className="px-3 py-2 text-left font-semibold">描述</th>
|
|
|
- <th className="px-3 py-2 text-left font-semibold">金额范围</th>
|
|
|
- <th className="px-3 py-2 text-left font-semibold">处理时间</th>
|
|
|
- <th className="px-3 py-2 text-left font-semibold">费用</th>
|
|
|
- <th className="px-3 py-2 text-right font-semibold">操作</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- {items.map((item) => (
|
|
|
- <tr key={item.id} className="border-t border-[var(--border)]">
|
|
|
- <td className="px-3 py-2">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- {item.icon ? (
|
|
|
- // eslint-disable-next-line @next/next/no-img-element
|
|
|
- <img
|
|
|
- src={item.icon}
|
|
|
- alt={item.name || item.code}
|
|
|
- className="h-7 w-7 rounded object-contain"
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <span className="inline-block h-7 w-7 rounded bg-slate-100 text-center leading-7">
|
|
|
- -
|
|
|
- </span>
|
|
|
- )}
|
|
|
- <span>{item.name || item.code}</span>
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- <td className="px-3 py-2 text-[var(--muted)]">{item.enName || item.name || "-"}</td>
|
|
|
- <td className="px-3 py-2">{formatAmountRange(item)}</td>
|
|
|
- <td className="px-3 py-2">{item.fundingTime || "1 hours"}</td>
|
|
|
- <td className="px-3 py-2">{formatFee(item)}</td>
|
|
|
- <td className="px-3 py-2 text-right">
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={() => {
|
|
|
- setSelectedChannelId(item.id);
|
|
|
- setApplyDialogOpen(true);
|
|
|
- }}
|
|
|
- className={`ui-interactive-btn rounded border px-4 py-1 text-xs font-semibold ${
|
|
|
- selectedChannelId === item.id
|
|
|
- ? "border-[var(--navy)] bg-[var(--navy)] text-white"
|
|
|
- : "border-[var(--border)] bg-white text-[var(--navy)] hover:bg-slate-50"
|
|
|
- }`}
|
|
|
- >
|
|
|
- 选择
|
|
|
- </button>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- ))}
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
- {items.length === 0 ? <p className="px-3 py-3 text-sm text-[var(--muted)]">暂无通道</p> : null}
|
|
|
- </div>
|
|
|
+ <span className="text-lg">{group}</span>
|
|
|
+ <span className={cn("transition-transform duration-300 text-slate-500", expandedGroup === group ? "rotate-180 text-[#f3deae]" : "rotate-0")}>▼</span>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ {expandedGroup === group && (
|
|
|
+ <div className="border-t border-white/5 bg-black/20 p-4 md:p-6">
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <table className="w-full min-w-[800px] text-sm text-left">
|
|
|
+ <thead className="text-slate-400 border-b border-white/10">
|
|
|
+ <tr>
|
|
|
+ <th className="pb-3 px-4 font-semibold">通道类型</th>
|
|
|
+ <th className="pb-3 px-4 font-semibold">限额</th>
|
|
|
+ <th className="pb-3 px-4 font-semibold">手续费</th>
|
|
|
+ <th className="pb-3 px-4 font-semibold text-right">操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody className="divide-y divide-white/5">
|
|
|
+ {items.map((item) => (
|
|
|
+ <tr key={item.id} className="hover:bg-white/5 transition-colors">
|
|
|
+ <td className="py-4 px-4">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ {item.icon ? <img src={item.icon} alt="" className="h-8 w-8 rounded-lg bg-white p-1" /> : <div className="h-8 w-8 rounded-lg bg-white/10" />}
|
|
|
+ <span className="font-bold text-slate-200">{item.name || item.code}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td className="py-4 px-4 font-medium text-slate-300">{formatAmountRange(item)}</td>
|
|
|
+ <td className="py-4 px-4 font-medium text-[#b89458]">{formatFee(item)}</td>
|
|
|
+ <td className="py-4 px-4 text-right">
|
|
|
+ <button
|
|
|
+ onClick={() => { setSelectedChannelId(item.id); setApplyDialogOpen(true); }}
|
|
|
+ className={cn("rounded-full px-5 py-2 text-xs font-bold transition-all", selectedChannelId === item.id ? "bg-[#f3deae] text-[#5c461a]" : "bg-white/10 text-white hover:bg-white/20")}
|
|
|
+ >
|
|
|
+ {selectedChannelId === item.id ? "已选定" : "选择通道"}
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <p className="mt-8 text-center text-sm">
|
|
|
- <Link href="/account" className="text-[var(--accent)] hover:underline">
|
|
|
- 返回用户中心
|
|
|
- </Link>
|
|
|
- </p>
|
|
|
-
|
|
|
- {selectedChannel ? (
|
|
|
- <ModalShell open={confirmOpen} className="max-w-md" zIndexClassName="z-[70]">
|
|
|
- <div className="w-full rounded-2xl border border-[var(--border)] bg-[var(--card)] p-5 shadow-xl">
|
|
|
- <p className="text-base font-semibold text-[var(--navy)]">确认提交提款申请?</p>
|
|
|
- <div className="mt-3 space-y-1 text-sm text-[var(--muted)]">
|
|
|
- <p>提款通道:{selectedChannel.name || selectedChannel.enName || selectedChannel.code}</p>
|
|
|
- <p>
|
|
|
- 提款金额:{amount} {selectedChannel.currency || "USD"}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <div className="mt-5 flex justify-end gap-2">
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={() => setConfirmOpen(false)}
|
|
|
- className="rounded-lg border border-[var(--border)] px-4 py-2 text-sm"
|
|
|
- >
|
|
|
- 取消
|
|
|
- </button>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={() => void doSubmit()}
|
|
|
- className="rounded-lg bg-[var(--navy)] px-4 py-2 text-sm text-white"
|
|
|
- >
|
|
|
- 确认提交
|
|
|
- </button>
|
|
|
- </div>
|
|
|
+ ))}
|
|
|
</div>
|
|
|
- </ModalShell>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {selectedChannel ? (
|
|
|
- <ModalShell open={applyDialogOpen} className="max-w-2xl" zIndexClassName="z-[60]">
|
|
|
- <section
|
|
|
- className="w-full rounded-2xl border border-[var(--border)] bg-[var(--card)] p-4 shadow-xl"
|
|
|
- >
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <h2 className="text-sm font-semibold text-[var(--navy)]">填写提款信息</h2>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={() => {
|
|
|
- resetApplyForm();
|
|
|
- setApplyDialogOpen(false);
|
|
|
- }}
|
|
|
- className="ui-interactive-btn rounded border border-[var(--border)] px-2 py-1 text-xs text-[var(--muted)]"
|
|
|
- >
|
|
|
- 关闭
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div className="mt-3 rounded-lg border border-[var(--border)] bg-slate-50 px-3 py-2 text-sm text-[var(--navy)]">
|
|
|
- 钱包余额:
|
|
|
- <span className="ml-1 font-semibold tabular-nums">
|
|
|
- {walletBalanceLoading ? "加载中..." : `$${(walletBalance ?? 0).toFixed(2)}`}
|
|
|
- </span>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 填写表单大弹窗 */}
|
|
|
+ {applyDialogOpen && selectedChannel && (
|
|
|
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#050b14]/90 p-4 backdrop-blur-md overflow-y-auto">
|
|
|
+ <div className="my-auto w-full max-w-2xl overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
|
|
|
+ <div className="flex items-center justify-between border-b border-white/5 p-6 md:px-8">
|
|
|
+ <h2 className="text-xl font-bold text-white flex items-center gap-2"><Wallet className="text-[#b89458]"/> 填写提款信息</h2>
|
|
|
+ <button onClick={() => setApplyDialogOpen(false)} className="text-sm font-bold text-slate-500 hover:text-white">关闭</button>
|
|
|
</div>
|
|
|
- <div className="mt-3 rounded-lg border border-[var(--border)] bg-slate-50 px-3 py-2 text-sm text-[var(--navy)]">
|
|
|
- 已选通道:
|
|
|
- <span className="ml-1 font-semibold">
|
|
|
- {selectedChannel.name || selectedChannel.enName || selectedChannel.code}
|
|
|
- </span>
|
|
|
+
|
|
|
+ <div className="p-6 md:p-8 max-h-[70vh] overflow-y-auto space-y-6 custom-scrollbar">
|
|
|
+ {/* 提示信息 */}
|
|
|
+ <div className="rounded-2xl border border-[#b89458]/30 bg-[#b89458]/10 p-5 text-sm text-[#f3deae] leading-relaxed">
|
|
|
+ 当前通道: <span className="font-bold ml-1">{selectedChannel.name || selectedChannel.code}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表单渲染区域 */}
|
|
|
+ <div className="space-y-5">
|
|
|
+ {isDigitalCurrencyType(selectedChannel.type) && (
|
|
|
+ <div className="grid gap-5 md:grid-cols-2">
|
|
|
+ <div><label className="text-sm font-bold text-slate-300">区块链网络</label><input className={InputCls} placeholder="例如: TRC20" value={addressName} onChange={(e)=>setAddressName(e.target.value)} /></div>
|
|
|
+ <div><label className="text-sm font-bold text-slate-300">收款钱包地址</label><input className={InputCls} placeholder="请输入您的钱包地址" value={address} onChange={(e)=>setAddress(e.target.value)} /></div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {isBankType(selectedChannel.type) && (
|
|
|
+ <div className="grid gap-5 md:grid-cols-2">
|
|
|
+ <div><label className="text-sm font-bold text-slate-300">开户姓名</label><input className={InputCls} value={bankUnameInput} onChange={(e)=>setBankUnameInput(e.target.value)} /></div>
|
|
|
+ <div><label className="text-sm font-bold text-slate-300">银行卡号</label><input className={InputCls} value={bankCardNumInput} onChange={(e)=>setBankCardNumInput(e.target.value)} /></div>
|
|
|
+ <div><label className="text-sm font-bold text-slate-300">银行名称</label><input className={InputCls} value={bankNameInput} onChange={(e)=>setBankNameInput(e.target.value)} /></div>
|
|
|
+ <div><label className="text-sm font-bold text-slate-300">支行信息</label><input className={InputCls} value={bankBranchNameInput} onChange={(e)=>setBankBranchNameInput(e.target.value)} /></div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 金额 */}
|
|
|
+ <div>
|
|
|
+ <label className="text-sm font-bold text-slate-300">提取金额 ({selectedChannel.currency || "USD"})</label>
|
|
|
+ <div className="relative mt-2">
|
|
|
+ <span className="absolute left-4 top-3.5 text-slate-500 font-bold">$</span>
|
|
|
+ <input type="number" className={cn(InputCls, "pl-8 text-xl font-bold text-[#f3deae] mt-0")} placeholder="0.00" value={amount} onChange={(e)=>setAmount(e.target.value)} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 条款 */}
|
|
|
+ <div className="mt-6 space-y-4 rounded-2xl bg-white/5 p-5">
|
|
|
+ <label className="flex items-start gap-3 text-sm text-slate-400 cursor-pointer group">
|
|
|
+ <input type="checkbox" checked={agree} onChange={(e)=>setAgree(e.target.checked)} className="mt-1 h-4 w-4 rounded border-white/20 bg-black/50 text-[#b89458] focus:ring-[#b89458]" />
|
|
|
+ <span className="group-hover:text-slate-300 transition-colors">我已仔细核对上述收款信息,因填写错误导致的资金损失由本人承担。</span>
|
|
|
+ </label>
|
|
|
+ <label className="flex items-start gap-3 text-sm text-slate-400 cursor-pointer group">
|
|
|
+ <input type="checkbox" checked={agreeExtra} onChange={(e)=>setAgreeExtra(e.target.checked)} className="mt-1 h-4 w-4 rounded border-white/20 bg-black/50 text-[#b89458] focus:ring-[#b89458]" />
|
|
|
+ <span className="group-hover:text-slate-300 transition-colors">我知悉并同意提款手续费规则,且了解资金到账受区块网络/银行处理时间影响。</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
- {selectedChannel.introduce || selectedChannel.enIntroduce ? (
|
|
|
- <div
|
|
|
- className="mt-3 rounded-lg border border-[var(--border)] bg-slate-50 p-3 text-sm leading-7 text-[var(--navy)]"
|
|
|
- dangerouslySetInnerHTML={{
|
|
|
- __html: sanitizeHtml(selectedChannel.introduce || selectedChannel.enIntroduce || ""),
|
|
|
- }}
|
|
|
- />
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {bankOptions.length > 0 ? (
|
|
|
- <div className="mt-3">
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行通道</label>
|
|
|
- <select
|
|
|
- value={selectedBankCode}
|
|
|
- onChange={(e) => setSelectedBankCode(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- >
|
|
|
- <option value="">请选择银行通道</option>
|
|
|
- {bankOptions.map((item) => (
|
|
|
- <option key={item.code} value={item.code}>
|
|
|
- {item.name || item.enName || item.code}
|
|
|
- </option>
|
|
|
- ))}
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {shouldShowSavedAccountSelector ? (
|
|
|
- <div className="mt-3">
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">收款信息</label>
|
|
|
- <select
|
|
|
- value={selectedSavedId}
|
|
|
- onChange={(e) => setSelectedSavedId(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- >
|
|
|
- <option value="">请选择收款信息</option>
|
|
|
- {filteredSavedAccounts.map((item) => (
|
|
|
- <option key={item.id} value={item.id} disabled={item.type === 4 && item.authStatus === 0}>
|
|
|
- {item.type === 4
|
|
|
- ? `${item.addressName || "-"} - ${item.address || "-"}`
|
|
|
- : `${item.bankName || "-"} - ${item.bankCardNum || "-"}`}
|
|
|
- </option>
|
|
|
- ))}
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {isBankType(selectedChannel.type) ? (
|
|
|
- <div className="mt-3 grid gap-3 md:grid-cols-2">
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">户名</label>
|
|
|
- <input
|
|
|
- value={bankUnameInput}
|
|
|
- onChange={(e) => setBankUnameInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行卡号</label>
|
|
|
- <input
|
|
|
- value={bankCardNumInput}
|
|
|
- onChange={(e) => setBankCardNumInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行名称</label>
|
|
|
- <input
|
|
|
- value={bankNameInput}
|
|
|
- onChange={(e) => setBankNameInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">支行名称</label>
|
|
|
- <input
|
|
|
- value={bankBranchNameInput}
|
|
|
- onChange={(e) => setBankBranchNameInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {selectedChannel.type === "BANK_TELEGRAPHIC" ? (
|
|
|
- <div className="mt-3 grid gap-3 md:grid-cols-3">
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">户名</label>
|
|
|
- <input
|
|
|
- value={bankUnameInput}
|
|
|
- onChange={(e) => setBankUnameInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行卡号</label>
|
|
|
- <input
|
|
|
- value={bankCardNumInput}
|
|
|
- onChange={(e) => setBankCardNumInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行名称</label>
|
|
|
- <input
|
|
|
- value={bankNameInput}
|
|
|
- onChange={(e) => setBankNameInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">Swift Code</label>
|
|
|
- <input
|
|
|
- value={swiftCodeInput}
|
|
|
- onChange={(e) => setSwiftCodeInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行代码</label>
|
|
|
- <input
|
|
|
- value={customBankCodeInput}
|
|
|
- onChange={(e) => setCustomBankCodeInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">银行地址</label>
|
|
|
- <input
|
|
|
- value={bankAddrInput}
|
|
|
- onChange={(e) => setBankAddrInput(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {isCardType(selectedChannel.type) ? (
|
|
|
- <div className="mt-3 grid gap-3 md:grid-cols-2">
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">信用卡户名</label>
|
|
|
- <input
|
|
|
- value={cardUnameInput}
|
|
|
- onChange={(e) => setCardUnameInput(e.target.value)}
|
|
|
- placeholder="John Doe"
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">信用卡账户</label>
|
|
|
- <input
|
|
|
- value={cardNumInput}
|
|
|
- onChange={(e) => setCardNumInput(e.target.value)}
|
|
|
- placeholder="5188 5136 1855 2975"
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">CVV</label>
|
|
|
- <input
|
|
|
- value={cardCvvInput}
|
|
|
- onChange={(e) => setCardCvvInput(e.target.value)}
|
|
|
- placeholder="123"
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">到期年份/月 份</label>
|
|
|
- <input
|
|
|
- value={cardExpiryInput}
|
|
|
- onChange={(e) => setCardExpiryInput(e.target.value)}
|
|
|
- placeholder="30/09"
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {shouldRequireSavedAccount && !shouldShowSavedAccountSelector ? (
|
|
|
- <p className="mt-3 text-xs text-[var(--muted)]">
|
|
|
- 当前通道暂无可用收款信息,请更换通道或先补充收款信息。
|
|
|
- </p>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {isWalletType(selectedChannel.type) ? (
|
|
|
- <div className="mt-3">
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">提款地址</label>
|
|
|
- <input
|
|
|
- value={address}
|
|
|
- onChange={(e) => setAddress(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {isDigitalCurrencyType(selectedChannel.type) ? (
|
|
|
- <div className="mt-3 grid gap-3 md:grid-cols-2">
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">区块链名称</label>
|
|
|
- <input
|
|
|
- value={addressName}
|
|
|
- onChange={(e) => setAddressName(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">钱包地址</label>
|
|
|
- <input
|
|
|
- value={address}
|
|
|
- onChange={(e) => setAddress(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {isBankTelegraphic ? (
|
|
|
- <div className="mt-3 grid gap-3 md:grid-cols-2">
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">货币类型</label>
|
|
|
- <select
|
|
|
- value={telegraphicCurrency}
|
|
|
- onChange={(e) => setTelegraphicCurrency(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
|
|
|
- >
|
|
|
- <option value="USD">USD</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">金额</label>
|
|
|
- <input
|
|
|
- value={amount}
|
|
|
- onChange={(e) => setAmount(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className="mt-3">
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">
|
|
|
- 提款金额({selectedChannel.currency || "USD"})
|
|
|
- </label>
|
|
|
- <input
|
|
|
- value={amount}
|
|
|
- onChange={(e) => setAmount(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {isBankTelegraphic ? (
|
|
|
- <div className="mt-3 grid gap-3 md:grid-cols-2">
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">Account Agency NO</label>
|
|
|
- <input
|
|
|
- value={agencyNo}
|
|
|
- onChange={(e) => setAgencyNo(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- {needCpf ? (
|
|
|
- <div>
|
|
|
- <label className="text-sm font-medium text-[var(--navy)]">CPF</label>
|
|
|
- <input
|
|
|
- value={cpf}
|
|
|
- onChange={(e) => setCpf(e.target.value)}
|
|
|
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {
|
|
|
- <label className="mt-3 flex items-start gap-2 text-sm text-[var(--navy)]">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- checked={agree}
|
|
|
- onChange={(e) => setAgree(e.target.checked)}
|
|
|
- className="mt-0.5"
|
|
|
- />
|
|
|
- <span>我已阅读并同意提款条款,知悉手续费与到账时间以平台审核为准。</span>
|
|
|
- </label>
|
|
|
- }
|
|
|
-
|
|
|
- {
|
|
|
- <label className="mt-2 flex items-start gap-2 text-sm text-[var(--navy)]">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- checked={agreeExtra}
|
|
|
- onChange={(e) => setAgreeExtra(e.target.checked)}
|
|
|
- className="mt-0.5"
|
|
|
- />
|
|
|
- <span>* 我确认本次提款信息准确无误,并接受平台审核结果。</span>
|
|
|
- </label>
|
|
|
- }
|
|
|
-
|
|
|
- {
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- disabled={submitting}
|
|
|
- onClick={() => {
|
|
|
- const msg = validate();
|
|
|
- if (msg) {
|
|
|
- setResultDialog({
|
|
|
- open: true,
|
|
|
- status: "error",
|
|
|
- title: "请检查输入信息",
|
|
|
- message: msg,
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
- setConfirmOpen(true);
|
|
|
- }}
|
|
|
- className="ui-interactive-btn mt-4 w-full rounded-full bg-[var(--navy)] py-3 text-sm font-semibold text-white disabled:opacity-60"
|
|
|
+
|
|
|
+ <div className="border-t border-white/5 p-6 bg-black/20 flex gap-4">
|
|
|
+ <button onClick={() => setApplyDialogOpen(false)} className="flex-1 rounded-xl bg-white/10 py-4 font-bold text-white hover:bg-white/20 transition-all">取消</button>
|
|
|
+ <button
|
|
|
+ onClick={() => { const msg = validate(); if(msg) { alert(msg); return; } setConfirmOpen(true); }}
|
|
|
+ disabled={submitting}
|
|
|
+ className="flex-[2] rounded-xl bg-gradient-to-br from-[#f3deae] to-[#d9be88] py-4 font-bold text-[#5c461a] shadow-lg hover:opacity-90 transition-all disabled:opacity-50"
|
|
|
>
|
|
|
- {submitting ? "提交中..." : "提交提款申请"}
|
|
|
+ 校验并进入最后确认
|
|
|
</button>
|
|
|
- }
|
|
|
- </section>
|
|
|
- </ModalShell>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- <ModalShell open={resultDialog.open} className="max-w-md" zIndexClassName="z-[80]">
|
|
|
- <div className="w-full overflow-hidden rounded-2xl border border-[var(--border)] bg-[var(--card)] shadow-2xl">
|
|
|
- <div
|
|
|
- className={`px-5 py-4 ${
|
|
|
- resultDialog.status === "success"
|
|
|
- ? "bg-gradient-to-r from-emerald-500/10 to-emerald-400/5"
|
|
|
- : "bg-gradient-to-r from-rose-500/10 to-rose-400/5"
|
|
|
- }`}
|
|
|
- >
|
|
|
- <div className="flex items-center gap-3">
|
|
|
- <span
|
|
|
- className={`inline-flex h-8 w-8 items-center justify-center rounded-full text-base font-bold ${
|
|
|
- resultDialog.status === "success"
|
|
|
- ? "bg-emerald-100 text-emerald-700"
|
|
|
- : "bg-rose-100 text-rose-700"
|
|
|
- }`}
|
|
|
- >
|
|
|
- {resultDialog.status === "success" ? "✓" : "!"}
|
|
|
- </span>
|
|
|
- <p className="text-base font-semibold text-[var(--navy)]">{resultDialog.title}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className="px-5 py-4">
|
|
|
- <p className="text-sm leading-6 text-[var(--muted)]">{resultDialog.message}</p>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={() => setResultDialog((prev) => ({ ...prev, open: false }))}
|
|
|
- className={`ui-interactive-btn mt-5 w-full rounded-full py-2.5 text-sm font-semibold text-white ${
|
|
|
- resultDialog.status === "success"
|
|
|
- ? "bg-emerald-600 hover:bg-emerald-700"
|
|
|
- : "bg-[var(--navy)] hover:bg-[var(--navy-soft)]"
|
|
|
- }`}
|
|
|
- >
|
|
|
- 我知道了
|
|
|
- </button>
|
|
|
- </div>
|
|
|
</div>
|
|
|
- </ModalShell>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 二次确认弹窗 */}
|
|
|
+ {confirmOpen && (
|
|
|
+ <div className="fixed inset-0 z-[60] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
|
|
|
+ <div className="w-full max-w-sm rounded-[2.5rem] border border-white/10 bg-[#0a1120] text-center shadow-2xl overflow-hidden">
|
|
|
+ <div className="p-10">
|
|
|
+ <div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-[#b89458]/10 text-[#f3deae]"><AlertCircle size={32}/></div>
|
|
|
+ <h3 className="text-xl font-bold text-white mb-2">即将发起提款</h3>
|
|
|
+ <p className="text-[#f3deae] text-3xl font-bold font-serif mb-4">${amount}</p>
|
|
|
+ <p className="text-sm text-slate-400">请再次确认信息无误,提交后将进入人工审核队列。</p>
|
|
|
+ </div>
|
|
|
+ <div className="flex border-t border-white/5">
|
|
|
+ <button onClick={() => setConfirmOpen(false)} className="flex-1 py-5 font-bold text-slate-500 hover:bg-white/5">返回修改</button>
|
|
|
+ <div className="w-px bg-white/5" />
|
|
|
+ <button onClick={doSubmit} disabled={submitting} className="flex-1 py-5 font-bold text-[#f3deae] hover:bg-white/5 disabled:opacity-50">{submitting ? "提交中..." : "确认无误提交"}</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 结果弹窗 */}
|
|
|
+ {resultDialog.open && (
|
|
|
+ <div className="fixed inset-0 z-[70] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
|
|
|
+ <div className="w-full max-w-sm rounded-[2.5rem] border border-white/10 bg-[#0a1120] text-center shadow-2xl overflow-hidden p-10">
|
|
|
+ <div className={cn("mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full", resultDialog.status === 'success' ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400")}>
|
|
|
+ {resultDialog.status === 'success' ? <CheckCircle2 size={32}/> : <AlertCircle size={32}/>}
|
|
|
+ </div>
|
|
|
+ <h3 className="text-xl font-bold text-white mb-3">{resultDialog.title}</h3>
|
|
|
+ <p className="text-sm text-slate-400 mb-8">{resultDialog.message}</p>
|
|
|
+ <button onClick={() => { setResultDialog(p => ({...p, open: false})); if(resultDialog.status==='success') setApplyDialogOpen(false); }} className="w-full rounded-xl bg-white/10 py-4 font-bold text-white hover:bg-white/20">我知道了</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
</div>
|
|
|
);
|
|
|
-}
|
|
|
+}
|