feat: add dist-site commission withdraw

This commit is contained in:
abingyyds committed 2026-04-08 14:17:08 +08:00
1 parent de5d047371
commit f95687bc2d
3 files changed
+205 -4

No files matched your search

+16
View File
@@ -298,6 +298,22 @@
"topup.transferPlaceholder": "Enter transfer amount (quota)",
"topup.transfer": "Transfer",
"topup.transferSuccess": "Transfer successful",
"topup.withdraw": "Withdraw",
"topup.withdrawTitle": "Request Withdrawal",
"topup.withdrawSubtitle": "Submit a payout request with your preferred payment method",
"topup.withdrawAvailable": "Withdrawable Balance",
"topup.withdrawAmount": "Withdrawal Amount",
"topup.withdrawAll": "Withdraw All",
"topup.withdrawMethod": "Payment Method",
"topup.withdrawMethodPlaceholder": "e.g. Alipay, WeChat, bank card",
"topup.withdrawRemark": "Account / Note",
"topup.withdrawRemarkPlaceholder": "Enter your payout account or extra notes",
"topup.submitWithdraw": "Submit Withdrawal",
"topup.withdrawSuccess": "Withdrawal request submitted",
"topup.invalidWithdrawAmount": "Please enter a valid withdrawal amount",
"topup.withdrawExceedsBalance": "Withdrawal amount cannot exceed your withdrawable balance",
"topup.enterWithdrawMethod": "Please enter a payment method",
"topup.currentCommissionRateLabel": "Commission Rate",
"dashboard.topupDesc": "Recharge account balance",
+16
View File
@@ -298,6 +298,22 @@
"topup.transferPlaceholder": "输入划转额度 (quota)",
"topup.transfer": "划转",
"topup.transferSuccess": "划转成功",
"topup.withdraw": "提现",
"topup.withdrawTitle": "申请提现",
"topup.withdrawSubtitle": "填写收款方式后即可提交提现申请",
"topup.withdrawAvailable": "可提现余额",
"topup.withdrawAmount": "提现金额",
"topup.withdrawAll": "全部提现",
"topup.withdrawMethod": "收款方式",
"topup.withdrawMethodPlaceholder": "如:支付宝、微信、银行卡",
"topup.withdrawRemark": "收款账号 / 备注",
"topup.withdrawRemarkPlaceholder": "请输入收款账号或补充说明",
"topup.submitWithdraw": "提交提现",
"topup.withdrawSuccess": "提现申请已提交",
"topup.invalidWithdrawAmount": "请输入有效的提现金额",
"topup.withdrawExceedsBalance": "提现金额不能超过可提现余额",
"topup.enterWithdrawMethod": "请输入收款方式",
"topup.currentCommissionRateLabel": "当前返佣比例",
"dashboard.topupDesc": "充值账户余额",
+173 -4
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import { getUserUsage, redeemCode, getAffCode, transferAffQuota, getAffEarnings, Q, quotaToDollar } from '../api';
import { getUserUsage, redeemCode, getAffCode, transferAffQuota, getAffEarnings, requestAffWithdraw, Q } from '../api';
import { useCurrency, useSite } from '../context/SiteContext';
import CountUp from '../components/bits/CountUp';
import toast from 'react-hot-toast';
@@ -23,6 +23,11 @@ export default function Dashboard() {
const [affEarningsLoading, setAffEarningsLoading] = useState(false);
const [transferAmount, setTransferAmount] = useState('');
const [transferring, setTransferring] = useState(false);
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [withdrawAmount, setWithdrawAmount] = useState('');
const [withdrawMethod, setWithdrawMethod] = useState('');
const [withdrawRemark, setWithdrawRemark] = useState('');
const [withdrawing, setWithdrawing] = useState(false);
const loadData = useCallback(async () => {
try {
@@ -92,10 +97,61 @@ export default function Dashboard() {
setTransferring(false);
};
const resetWithdrawForm = () => {
setWithdrawAmount('');
setWithdrawMethod('');
setWithdrawRemark('');
};
const handleOpenWithdraw = () => {
resetWithdrawForm();
setShowWithdrawModal(true);
};
const handleCloseWithdraw = () => {
if (withdrawing) return;
resetWithdrawForm();
setShowWithdrawModal(false);
};
const quota = usage?.quota ?? user?.quota ?? 0;
const usedQuota = usage?.used_quota ?? user?.used_quota ?? 0;
const requestCount = usage?.request_count ?? user?.request_count ?? 0;
const balanceDollars = quota / Q * rate;
const availableAffAmount = ((user?.aff_quota || 0) / Q) * rate;
const currentCommissionRate = Number(user?.commission_rate || 0);
const handleWithdraw = async () => {
const amount = Number.parseFloat(withdrawAmount);
if (!Number.isFinite(amount) || amount <= 0) {
toast.error(t('topup.invalidWithdrawAmount'));
return;
}
if (amount - availableAffAmount > 1e-8) {
toast.error(t('topup.withdrawExceedsBalance'));
return;
}
if (!withdrawMethod.trim()) {
toast.error(t('topup.enterWithdrawMethod'));
return;
}
setWithdrawing(true);
try {
const res = await requestAffWithdraw({
amount: amount / rate,
payment_method: withdrawMethod.trim(),
remark: withdrawRemark.trim(),
});
if (res.data.success) {
toast.success(res.data.message || t('topup.withdrawSuccess'));
setShowWithdrawModal(false);
resetWithdrawForm();
await Promise.all([loadData(), refreshUser()]);
}
} catch (err) { /* interceptor */ }
setWithdrawing(false);
};
return (
<div className="max-w-7xl mx-auto px-6 py-10">
@@ -189,8 +245,16 @@ export default function Dashboard() {
{/* Invitation / Affiliate */}
{affLink && (
<div className="glass rounded-2xl p-6 mt-6">
<h2 className="text-lg font-semibold text-page mb-1">{t('topup.inviteTitle')}</h2>
<p className="text-sm text-page-secondary mb-5">{t('topup.inviteSubtitle')}</p>
<div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 className="text-lg font-semibold text-page mb-1">{t('topup.inviteTitle')}</h2>
<p className="text-sm text-page-secondary">{t('topup.inviteSubtitle')}</p>
</div>
<div className="inline-flex items-center gap-2 self-start rounded-full border border-brand-500/20 bg-brand-500/10 px-3 py-1 text-xs font-medium text-page">
<span className="text-page-secondary">{t('topup.currentCommissionRateLabel')}</span>
<span className="text-page-link">{(currentCommissionRate * 100).toFixed(1)}%</span>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
@@ -234,7 +298,16 @@ export default function Dashboard() {
{/* Transfer to balance */}
{(user?.aff_quota || 0) > 0 && (
<div className="mb-5">
<label className="block text-sm font-medium text-page-label mb-2">{t('topup.transferToBalance')}</label>
<div className="mb-2 flex items-center justify-between gap-3">
<label className="block text-sm font-medium text-page-label">{t('topup.transferToBalance')}</label>
<button
type="button"
onClick={handleOpenWithdraw}
className="btn-secondary whitespace-nowrap px-4 py-2 text-sm"
>
{t('topup.withdraw')}
</button>
</div>
<div className="flex gap-2">
<input
type="number"
@@ -293,6 +366,102 @@ export default function Dashboard() {
</div>
</div>
)}
{showWithdrawModal && (
<div
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
onClick={handleCloseWithdraw}
>
<div
className="glass w-full max-w-md rounded-2xl p-6"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-5">
<h3 className="text-lg font-semibold text-page mb-1">{t('topup.withdrawTitle')}</h3>
<p className="text-sm text-page-secondary">{t('topup.withdrawSubtitle')}</p>
</div>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-page-label">{t('topup.withdrawAvailable')}</label>
<input
type="text"
readOnly
value={`${symbol}${availableAffAmount.toFixed(2)}`}
className="input bg-page-surface-hover/60 text-page-secondary"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-page-label">{t('topup.withdrawAmount')}</label>
<div className="flex gap-2">
<div className="relative flex-1">
<span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-page-muted">
{symbol}
</span>
<input
type="number"
min="0"
step="0.01"
value={withdrawAmount}
onChange={(e) => setWithdrawAmount(e.target.value)}
placeholder="0.00"
className="input pl-8"
/>
</div>
<button
type="button"
onClick={() => setWithdrawAmount(availableAffAmount.toFixed(2))}
className="btn-secondary whitespace-nowrap px-4"
>
{t('topup.withdrawAll')}
</button>
</div>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-page-label">{t('topup.withdrawMethod')}</label>
<input
type="text"
value={withdrawMethod}
onChange={(e) => setWithdrawMethod(e.target.value)}
placeholder={t('topup.withdrawMethodPlaceholder')}
className="input"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-page-label">{t('topup.withdrawRemark')}</label>
<textarea
value={withdrawRemark}
onChange={(e) => setWithdrawRemark(e.target.value)}
placeholder={t('topup.withdrawRemarkPlaceholder')}
className="input min-h-[96px] resize-y"
/>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<button
type="button"
onClick={handleCloseWithdraw}
disabled={withdrawing}
className="btn-secondary px-4 py-2"
>
{t('tokens.cancel')}
</button>
<button
type="button"
onClick={handleWithdraw}
disabled={withdrawing}
className="btn-primary px-4 py-2"
>
{withdrawing ? t('topup.processing') : t('topup.submitWithdraw')}
</button>
</div>
</div>
</div>
)}
</div>
);
}