|
|
@@ -0,0 +1,870 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import { Link } from "@/i18n/navigation";
|
|
|
+import { useEffect, useMemo, useState } from "react";
|
|
|
+import {
|
|
|
+ fetchSavedWithdrawAccounts,
|
|
|
+ fetchWithdrawBankOptions,
|
|
|
+ fetchWithdrawChannels,
|
|
|
+ submitWithdrawApply,
|
|
|
+ type SavedWithdrawAccount,
|
|
|
+ type WithdrawBankOption,
|
|
|
+ type WithdrawChannel,
|
|
|
+} from "@/lib/withdrawal-api";
|
|
|
+
|
|
|
+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 "数字货币";
|
|
|
+ if (type === "CHANNEL_TYPE_WALLET") return "电子钱包";
|
|
|
+ if (type === "CHANNEL_TYPE_CARD") return "信用卡";
|
|
|
+ if (type === "CHANNEL_TYPE_ALI_WALLET" || aliHint) return "支付宝";
|
|
|
+ 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;
|
|
|
+}
|
|
|
+
|
|
|
+function formatAmountRange(item: WithdrawChannel): string {
|
|
|
+ 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%";
|
|
|
+}
|
|
|
+
|
|
|
+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 needsSavedAccount(type: string): boolean {
|
|
|
+ return (
|
|
|
+ type === "BANK" ||
|
|
|
+ type === "BANK_TELEGRAPHIC" ||
|
|
|
+ type === "CHANNEL_TYPE_CARD" ||
|
|
|
+ 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;
|
|
|
+}
|
|
|
+
|
|
|
+export default function WithdrawApplyPage() {
|
|
|
+ const [channels, setChannels] = useState<WithdrawChannel[]>([]);
|
|
|
+ const [channelsLoading, setChannelsLoading] = useState(false);
|
|
|
+ const [channelsError, setChannelsError] = useState<string | null>(null);
|
|
|
+ const [savedAccountsError, setSavedAccountsError] = useState<string | null>(null);
|
|
|
+
|
|
|
+ const [savedAccounts, setSavedAccounts] = useState<SavedWithdrawAccount[]>([]);
|
|
|
+ const [bankOptions, setBankOptions] = useState<WithdrawBankOption[]>([]);
|
|
|
+
|
|
|
+ const [selectedChannelId, setSelectedChannelId] = useState("");
|
|
|
+ const [selectedSavedId, setSelectedSavedId] = useState("");
|
|
|
+ const [selectedBankCode, setSelectedBankCode] = useState("");
|
|
|
+ const [address, setAddress] = useState("");
|
|
|
+ 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("");
|
|
|
+ const [bankCardNumInput, setBankCardNumInput] = useState("");
|
|
|
+ const [bankNameInput, setBankNameInput] = useState("");
|
|
|
+ const [bankBranchNameInput, setBankBranchNameInput] = useState("");
|
|
|
+ const [swiftCodeInput, setSwiftCodeInput] = useState("");
|
|
|
+ const [customBankCodeInput, setCustomBankCodeInput] = useState("");
|
|
|
+ const [bankAddrInput, setBankAddrInput] = useState("");
|
|
|
+ const [telegraphicCurrency, setTelegraphicCurrency] = useState("USD");
|
|
|
+ const [cardUnameInput, setCardUnameInput] = useState("");
|
|
|
+ const [cardNumInput, setCardNumInput] = useState("");
|
|
|
+ const [cardCvvInput, setCardCvvInput] = useState("");
|
|
|
+ const [cardExpiryInput, setCardExpiryInput] = useState("");
|
|
|
+
|
|
|
+ const [submitting, setSubmitting] = useState(false);
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
+ const [success, setSuccess] = useState<string | null>(null);
|
|
|
+ const [confirmOpen, setConfirmOpen] = useState(false);
|
|
|
+ const [expandedGroup, setExpandedGroup] = useState<string>("数字货币");
|
|
|
+ const [applyDialogOpen, setApplyDialogOpen] = useState(false);
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }, [savedAccounts, selectedChannel]);
|
|
|
+
|
|
|
+ const shouldRequireSavedAccount = Boolean(selectedChannel && needsSavedAccount(selectedChannel.type));
|
|
|
+ const shouldShowSavedAccountSelector = shouldRequireSavedAccount && filteredSavedAccounts.length > 0;
|
|
|
+ const isBankTelegraphic = selectedChannel?.type === "BANK_TELEGRAPHIC";
|
|
|
+ const needCpf = selectedChannel?.code === "PAY_RETAILER_REMIT_PAY_KEY_BRW";
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ let cancelled = false;
|
|
|
+ async function loadBase() {
|
|
|
+ setChannelsLoading(true);
|
|
|
+ setChannelsError(null);
|
|
|
+ setSavedAccountsError(null);
|
|
|
+ try {
|
|
|
+ const [channelsResult, savedResult] = await Promise.allSettled([
|
|
|
+ fetchWithdrawChannels(),
|
|
|
+ fetchSavedWithdrawAccounts(),
|
|
|
+ ]);
|
|
|
+ if (cancelled) return;
|
|
|
+ if (channelsResult.status === "fulfilled") {
|
|
|
+ setChannels(channelsResult.value);
|
|
|
+ } else {
|
|
|
+ const err = channelsResult.reason as Error;
|
|
|
+ setChannelsError(err?.message || "提款通道加载失败");
|
|
|
+ setChannels([]);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (savedResult.status === "fulfilled") {
|
|
|
+ setSavedAccounts(savedResult.value);
|
|
|
+ } else {
|
|
|
+ const err = savedResult.reason as Error;
|
|
|
+ setSavedAccountsError(err?.message || "收款信息加载失败");
|
|
|
+ setSavedAccounts([]);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (!cancelled) {
|
|
|
+ setChannelsLoading(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ void loadBase();
|
|
|
+ return () => {
|
|
|
+ cancelled = true;
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!selectedChannel) {
|
|
|
+ setBankOptions([]);
|
|
|
+ setSelectedBankCode("");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentChannel = selectedChannel;
|
|
|
+ setSelectedSavedId("");
|
|
|
+ setAddress("");
|
|
|
+ setAgree(false);
|
|
|
+ setAgreeExtra(false);
|
|
|
+ setBankUnameInput("");
|
|
|
+ setBankCardNumInput("");
|
|
|
+ setBankNameInput("");
|
|
|
+ setBankBranchNameInput("");
|
|
|
+ setSwiftCodeInput("");
|
|
|
+ setCustomBankCodeInput("");
|
|
|
+ setBankAddrInput("");
|
|
|
+ setTelegraphicCurrency("USD");
|
|
|
+ setCardUnameInput("");
|
|
|
+ setCardNumInput("");
|
|
|
+ setCardCvvInput("");
|
|
|
+ setCardExpiryInput("");
|
|
|
+
|
|
|
+ if (!currentChannel.bankValid) {
|
|
|
+ setBankOptions([]);
|
|
|
+ setSelectedBankCode("");
|
|
|
+ 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([]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ void loadBankOptions();
|
|
|
+ return () => {
|
|
|
+ cancelled = true;
|
|
|
+ };
|
|
|
+ }, [selectedChannel]);
|
|
|
+
|
|
|
+ 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 || "");
|
|
|
+ }, [selectedSavedAccount, selectedChannel?.type, isBankTelegraphic]);
|
|
|
+
|
|
|
+ function validate(): string | null {
|
|
|
+ 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 (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 "请勾选第二条提款确认条款";
|
|
|
+ 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;
|
|
|
+ if (address.trim()) payload.address = address.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();
|
|
|
+ }
|
|
|
+ setSubmitting(true);
|
|
|
+ setError(null);
|
|
|
+ setSuccess(null);
|
|
|
+ try {
|
|
|
+ await submitWithdrawApply({
|
|
|
+ requestUrl: selectedChannel.requestUrl,
|
|
|
+ payload,
|
|
|
+ });
|
|
|
+ setSuccess("提款申请提交成功,请等待审核。");
|
|
|
+ setAmount("");
|
|
|
+ setAddress("");
|
|
|
+ setAgree(false);
|
|
|
+ setAgreeExtra(false);
|
|
|
+ setSelectedSavedId("");
|
|
|
+ setAgencyNo("");
|
|
|
+ setCpf("");
|
|
|
+ setBankUnameInput("");
|
|
|
+ setBankCardNumInput("");
|
|
|
+ setBankNameInput("");
|
|
|
+ setBankBranchNameInput("");
|
|
|
+ setSwiftCodeInput("");
|
|
|
+ setCustomBankCodeInput("");
|
|
|
+ setBankAddrInput("");
|
|
|
+ setTelegraphicCurrency("USD");
|
|
|
+ setCardUnameInput("");
|
|
|
+ setCardNumInput("");
|
|
|
+ setCardCvvInput("");
|
|
|
+ setCardExpiryInput("");
|
|
|
+ } catch (e) {
|
|
|
+ const err = e as Error;
|
|
|
+ setError(err.message || "提款申请失败,请稍后重试。");
|
|
|
+ } finally {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ return Object.entries(groups).sort((a, b) => groupOrder(a[0]) - groupOrder(b[0]));
|
|
|
+ }, [channels]);
|
|
|
+
|
|
|
+ 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>
|
|
|
+
|
|
|
+ <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 ? <p className="mt-2 text-sm text-[var(--muted)]">通道加载中...</p> : 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"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ ▼
|
|
|
+ </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={`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>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ {selectedChannel ? (
|
|
|
+ <p className="mt-3 text-sm text-emerald-700">
|
|
|
+ 已选择通道:{selectedChannel.name || selectedChannel.enName || selectedChannel.code}
|
|
|
+ </p>
|
|
|
+ ) : null}
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <p className="mt-8 text-center text-sm">
|
|
|
+ <Link href="/account" className="text-[var(--accent)] hover:underline">
|
|
|
+ 返回会员中心
|
|
|
+ </Link>
|
|
|
+ </p>
|
|
|
+
|
|
|
+ {selectedChannel ? (
|
|
|
+ <div
|
|
|
+ className={`fixed inset-0 z-[70] flex items-center justify-center px-4 transition-all duration-250 ${
|
|
|
+ confirmOpen ? "pointer-events-auto bg-black/40 opacity-100" : "pointer-events-none bg-black/0 opacity-0"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className={`w-full max-w-md rounded-2xl border border-[var(--border)] bg-[var(--card)] p-5 shadow-xl transition-all duration-250 ${
|
|
|
+ confirmOpen ? "translate-y-0 scale-100 opacity-100" : "translate-y-2 scale-[0.98] opacity-0"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ {selectedChannel ? (
|
|
|
+ <div
|
|
|
+ className={`fixed inset-0 z-[60] flex items-center justify-center px-4 transition-all duration-300 ${
|
|
|
+ applyDialogOpen ? "pointer-events-auto bg-black/40 opacity-100" : "pointer-events-none bg-black/0 opacity-0"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <section
|
|
|
+ className={`w-full max-w-2xl rounded-2xl border border-[var(--border)] bg-[var(--card)] p-4 shadow-xl transition-all duration-300 ${
|
|
|
+ applyDialogOpen ? "translate-y-0 scale-100 opacity-100" : "translate-y-3 scale-[0.98] opacity-0"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <h2 className="text-sm font-semibold text-[var(--navy)]">填写提款信息</h2>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => setApplyDialogOpen(false)}
|
|
|
+ className="rounded border border-[var(--border)] px-2 py-1 text-xs text-[var(--muted)]"
|
|
|
+ >
|
|
|
+ 关闭
|
|
|
+ </button>
|
|
|
+ </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}
|
|
|
+
|
|
|
+ {savedAccountsError && shouldRequireSavedAccount ? (
|
|
|
+ <p className="mt-1 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}
|
|
|
+
|
|
|
+ {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>
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ <>
|
|
|
+ {error ? <p className="mt-3 text-sm text-rose-700">{error}</p> : null}
|
|
|
+ {success ? <p className="mt-3 text-sm text-emerald-700">{success}</p> : null}
|
|
|
+ </>
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ disabled={submitting}
|
|
|
+ onClick={() => {
|
|
|
+ setError(null);
|
|
|
+ const msg = validate();
|
|
|
+ if (msg) {
|
|
|
+ setError(msg);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setConfirmOpen(true);
|
|
|
+ }}
|
|
|
+ className="mt-4 w-full rounded-full bg-[var(--navy)] py-3 text-sm font-semibold text-white disabled:opacity-60"
|
|
|
+ >
|
|
|
+ {submitting ? "提交中..." : "提交提款申请"}
|
|
|
+ </button>
|
|
|
+ }
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|