feat: 分站控制台增加邀请面板 + 充值快捷入口 + 完善i18n翻译
- 将邀请返佣面板从充值页移至控制台首页(Dashboard) - Dashboard 快捷入口新增充值链接 - 添加邀请相关中英文翻译(16个key) - 清理 Topup.jsx 中已移除的 aff 代码 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8eac39f9a
commit
1138015cc4
5 files changed
+266
-250
No files matched your search
@@ -243,6 +243,24 @@
|
||||
"topup.quotaIncluded": "{{count}} quota included",
|
||||
"topup.creemPayment": "Creem Payment",
|
||||
|
||||
"topup.inviteTitle": "Referral Program",
|
||||
"topup.inviteSubtitle": "Invite friends and earn ongoing commissions",
|
||||
"topup.affAvailable": "Available Commission",
|
||||
"topup.affTotal": "Total Commission",
|
||||
"topup.affCount": "Invited Users",
|
||||
"topup.inviteLink": "Invitation Link",
|
||||
"topup.copy": "Copy",
|
||||
"topup.copied": "Copied",
|
||||
"topup.viewEarnings": "View Earnings",
|
||||
"topup.hideEarnings": "Hide Earnings",
|
||||
"topup.noEarnings": "No earnings yet",
|
||||
"topup.transferToBalance": "Transfer to Balance",
|
||||
"topup.transferPlaceholder": "Enter transfer amount (quota)",
|
||||
"topup.transfer": "Transfer",
|
||||
"topup.transferSuccess": "Transfer successful",
|
||||
|
||||
"dashboard.topupDesc": "Recharge account balance",
|
||||
|
||||
"nav.logs": "Logs",
|
||||
|
||||
"dashboard.logs": "Call Logs",
|
||||
|
||||
@@ -243,6 +243,24 @@
|
||||
"topup.quotaIncluded": "含 {{count}} 额度",
|
||||
"topup.creemPayment": "Creem 支付",
|
||||
|
||||
"topup.inviteTitle": "邀请返佣",
|
||||
"topup.inviteSubtitle": "邀请好友注册,持续获得消费佣金",
|
||||
"topup.affAvailable": "可用佣金",
|
||||
"topup.affTotal": "累计佣金",
|
||||
"topup.affCount": "邀请人数",
|
||||
"topup.inviteLink": "邀请链接",
|
||||
"topup.copy": "复制",
|
||||
"topup.copied": "已复制",
|
||||
"topup.viewEarnings": "查看收入明细",
|
||||
"topup.hideEarnings": "收起明细",
|
||||
"topup.noEarnings": "暂无收入记录",
|
||||
"topup.transferToBalance": "划转到余额",
|
||||
"topup.transferPlaceholder": "输入划转额度 (quota)",
|
||||
"topup.transfer": "划转",
|
||||
"topup.transferSuccess": "划转成功",
|
||||
|
||||
"dashboard.topupDesc": "充值账户余额",
|
||||
|
||||
"nav.logs": "调用日志",
|
||||
|
||||
"dashboard.logs": "调用日志",
|
||||
|
||||
+174
-10
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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, Q, quotaToDollar } from '../api';
|
||||
import { getUserUsage, redeemCode, getAffCode, transferAffQuota, getAffEarnings, Q, quotaToDollar } from '../api';
|
||||
import { useCurrency } from '../context/SiteContext';
|
||||
import CountUp from '../components/bits/CountUp';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -15,14 +15,28 @@ export default function Dashboard() {
|
||||
const [redeemInput, setRedeemInput] = useState('');
|
||||
const [redeeming, setRedeeming] = useState(false);
|
||||
|
||||
const loadUsage = async () => {
|
||||
try {
|
||||
const r = await getUserUsage();
|
||||
if (r.data.success) setUsage(r.data.data);
|
||||
} catch (e) { /* interceptor */ }
|
||||
};
|
||||
// Invitation / Aff
|
||||
const [affLink, setAffLink] = useState('');
|
||||
const [affEarnings, setAffEarnings] = useState([]);
|
||||
const [showAffEarnings, setShowAffEarnings] = useState(false);
|
||||
const [affEarningsLoading, setAffEarningsLoading] = useState(false);
|
||||
const [transferAmount, setTransferAmount] = useState('');
|
||||
const [transferring, setTransferring] = useState(false);
|
||||
|
||||
useEffect(() => { loadUsage(); }, []);
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [usageRes, affRes] = await Promise.all([
|
||||
getUserUsage(),
|
||||
getAffCode().catch(() => null),
|
||||
]);
|
||||
if (usageRes.data.success) setUsage(usageRes.data.data);
|
||||
if (affRes?.data?.success && affRes.data.data) {
|
||||
setAffLink(`${window.location.origin}/register?aff=${affRes.data.data}`);
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const handleRedeem = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -33,12 +47,50 @@ export default function Dashboard() {
|
||||
if (res.data.success) {
|
||||
toast.success(t('dashboard.redeemSuccess'));
|
||||
setRedeemInput('');
|
||||
await Promise.all([loadUsage(), refreshUser()]);
|
||||
await Promise.all([loadData(), refreshUser()]);
|
||||
}
|
||||
} catch (err) { /* interceptor */ }
|
||||
setRedeeming(false);
|
||||
};
|
||||
|
||||
const loadAffEarnings = async () => {
|
||||
setAffEarningsLoading(true);
|
||||
try {
|
||||
const res = await getAffEarnings({ page: 1, page_size: 20 });
|
||||
if (res.data.success && res.data.data) {
|
||||
setAffEarnings(res.data.data);
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
setAffEarningsLoading(false);
|
||||
};
|
||||
|
||||
const handleCopyAffLink = () => {
|
||||
if (!affLink) return;
|
||||
navigator.clipboard.writeText(affLink).then(() => {
|
||||
toast.success(t('topup.copied'));
|
||||
}).catch(() => {
|
||||
toast.error('Copy failed');
|
||||
});
|
||||
};
|
||||
|
||||
const handleTransfer = async () => {
|
||||
const val = parseInt(transferAmount);
|
||||
if (!val || val <= 0) {
|
||||
toast.error(t('topup.enterAmount'));
|
||||
return;
|
||||
}
|
||||
setTransferring(true);
|
||||
try {
|
||||
const res = await transferAffQuota({ quota: val });
|
||||
if (res.data.success) {
|
||||
toast.success(res.data.message || t('topup.transferSuccess'));
|
||||
setTransferAmount('');
|
||||
await Promise.all([loadData(), refreshUser()]);
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
setTransferring(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;
|
||||
@@ -107,6 +159,10 @@ export default function Dashboard() {
|
||||
<p className="text-sm font-medium text-page group-hover:text-page-link transition-colors">{t('dashboard.apiKeys')}</p>
|
||||
<p className="text-xs text-page-muted">{t('dashboard.manageKeys')}</p>
|
||||
</Link>
|
||||
<Link to="/topup" className="glass-sm !rounded-xl px-4 py-3 hover:bg-page-surface-hover transition-colors group">
|
||||
<p className="text-sm font-medium text-page group-hover:text-page-link transition-colors">{t('nav.topup')}</p>
|
||||
<p className="text-xs text-page-muted">{t('dashboard.topupDesc')}</p>
|
||||
</Link>
|
||||
<Link to="/packages" className="glass-sm !rounded-xl px-4 py-3 hover:bg-page-surface-hover transition-colors group">
|
||||
<p className="text-sm font-medium text-page group-hover:text-page-link transition-colors">{t('dashboard.packages')}</p>
|
||||
<p className="text-xs text-page-muted">{t('dashboard.viewPlans')}</p>
|
||||
@@ -122,6 +178,114 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="glass-sm rounded-xl p-4 text-center">
|
||||
<p className="text-xs text-page-secondary mb-1">{t('topup.affAvailable')}</p>
|
||||
<p className="text-xl font-bold text-page">
|
||||
{symbol}{((user?.aff_quota || 0) / Q * rate).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-sm rounded-xl p-4 text-center">
|
||||
<p className="text-xs text-page-secondary mb-1">{t('topup.affTotal')}</p>
|
||||
<p className="text-xl font-bold text-page">
|
||||
{symbol}{((user?.aff_history_quota || 0) / Q * rate).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-sm rounded-xl p-4 text-center">
|
||||
<p className="text-xs text-page-secondary mb-1">{t('topup.affCount')}</p>
|
||||
<p className="text-xl font-bold text-page">{user?.aff_count || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invitation Link */}
|
||||
<div className="mb-5">
|
||||
<label className="block text-sm font-medium text-page-label mb-2">{t('topup.inviteLink')}</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={affLink}
|
||||
className="input flex-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyAffLink}
|
||||
className="btn-primary whitespace-nowrap text-sm px-4"
|
||||
>
|
||||
{t('topup.copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={transferAmount}
|
||||
onChange={(e) => setTransferAmount(e.target.value)}
|
||||
placeholder={t('topup.transferPlaceholder')}
|
||||
className="input flex-1 text-sm"
|
||||
min={1}
|
||||
/>
|
||||
<button
|
||||
onClick={handleTransfer}
|
||||
disabled={transferring}
|
||||
className="btn-primary whitespace-nowrap text-sm px-4"
|
||||
>
|
||||
{transferring ? t('topup.processing') : t('topup.transfer')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Earnings Details */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => { setShowAffEarnings(!showAffEarnings); if (!showAffEarnings) loadAffEarnings(); }}
|
||||
className="text-sm text-page-secondary hover:text-page transition-colors"
|
||||
>
|
||||
{showAffEarnings ? t('topup.hideEarnings') : t('topup.viewEarnings')}
|
||||
</button>
|
||||
{showAffEarnings && (
|
||||
<div className="mt-3">
|
||||
{affEarningsLoading ? (
|
||||
<div className="flex justify-center py-6">
|
||||
<div className="w-6 h-6 border-2 border-brand-500/30 border-t-brand-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
) : affEarnings.length === 0 ? (
|
||||
<p className="text-sm text-page-muted text-center py-6">{t('topup.noEarnings')}</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{affEarnings.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between glass-sm rounded-xl px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm text-page">{item.model_name}</p>
|
||||
<p className="text-xs text-page-muted">
|
||||
{new Date(item.created_time * 1000).toLocaleString()} · {(item.commission_rate * 100).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-page-success">
|
||||
+{symbol}{(item.commission_quota / Q * rate).toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+54
-238
@@ -6,7 +6,6 @@ import {
|
||||
getUserUsage, redeemCode, getTopupInfo, calculateAmount,
|
||||
createEpayOrder, createStripeOrder, createCreemOrder,
|
||||
createCryptoOrder, getCryptoOrderStatus, getTopupHistory,
|
||||
getAffCode, transferAffQuota, getAffEarnings,
|
||||
Q, quotaToDollar,
|
||||
} from '../api';
|
||||
import { useCurrency } from '../context/SiteContext';
|
||||
@@ -49,30 +48,18 @@ export default function Topup() {
|
||||
const [history, setHistory] = useState([]);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
|
||||
// Invitation / Aff
|
||||
const [affLink, setAffLink] = useState('');
|
||||
const [affEarnings, setAffEarnings] = useState([]);
|
||||
const [showAffEarnings, setShowAffEarnings] = useState(false);
|
||||
const [affEarningsLoading, setAffEarningsLoading] = useState(false);
|
||||
const [transferAmount, setTransferAmount] = useState('');
|
||||
const [transferring, setTransferring] = useState(false);
|
||||
|
||||
const enableTopup = site?.enable_topup && topupInfo;
|
||||
const topupConfig = site?.topup_config;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [usageRes, topupRes, affRes] = await Promise.all([
|
||||
const [usageRes, topupRes] = await Promise.all([
|
||||
getUserUsage(),
|
||||
site?.enable_topup ? getTopupInfo().catch(() => null) : Promise.resolve(null),
|
||||
getAffCode().catch(() => null),
|
||||
]);
|
||||
if (usageRes.data.success) setUsage(usageRes.data.data);
|
||||
if (topupRes?.data?.data) setTopupInfo(topupRes.data.data);
|
||||
if (affRes?.data?.success && affRes.data.data) {
|
||||
setAffLink(`${window.location.origin}/register?aff=${affRes.data.data}`);
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
setLoading(false);
|
||||
}, [site?.enable_topup]);
|
||||
@@ -304,45 +291,6 @@ export default function Topup() {
|
||||
setHistoryLoading(false);
|
||||
};
|
||||
|
||||
// Aff earnings
|
||||
const loadAffEarnings = async () => {
|
||||
setAffEarningsLoading(true);
|
||||
try {
|
||||
const res = await getAffEarnings({ page: 1, page_size: 20 });
|
||||
if (res.data.success && res.data.data) {
|
||||
setAffEarnings(res.data.data);
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
setAffEarningsLoading(false);
|
||||
};
|
||||
|
||||
const handleCopyAffLink = () => {
|
||||
if (!affLink) return;
|
||||
navigator.clipboard.writeText(affLink).then(() => {
|
||||
toast.success(t('topup.copied') || 'Copied!');
|
||||
}).catch(() => {
|
||||
toast.error('Copy failed');
|
||||
});
|
||||
};
|
||||
|
||||
const handleTransfer = async () => {
|
||||
const val = parseInt(transferAmount);
|
||||
if (!val || val <= 0) {
|
||||
toast.error(t('topup.enterAmount'));
|
||||
return;
|
||||
}
|
||||
setTransferring(true);
|
||||
try {
|
||||
const res = await transferAffQuota({ quota: val });
|
||||
if (res.data.success) {
|
||||
toast.success(res.data.message || t('topup.transferSuccess') || 'Transfer successful');
|
||||
setTransferAmount('');
|
||||
await Promise.all([loadData(), refreshUser()]);
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
setTransferring(false);
|
||||
};
|
||||
|
||||
const presetAmounts = topupInfo?.amount_options || [1, 5, 10, 20, 50, 100];
|
||||
const minTopup = topupInfo?.min_topup || 1;
|
||||
const payMethods = topupInfo?.pay_methods || [];
|
||||
@@ -398,8 +346,8 @@ export default function Topup() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Online Topup - EPay & Stripe */}
|
||||
{site?.enable_topup && (enableOnline || enableStripe) && epayAndStripeMethods.length > 0 && (
|
||||
{/* Online Topup - EPay & Stripe & Crypto */}
|
||||
{site?.enable_topup && (enableOnline || enableStripe || enableCrypto) && (epayAndStripeMethods.length > 0 || enableCrypto) && (
|
||||
<div className="glass rounded-2xl p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-page">{t('topup.onlineTopup')}</h2>
|
||||
@@ -482,6 +430,57 @@ export default function Topup() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Crypto Payment (inline) */}
|
||||
{enableCrypto && availableChains.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t border-page-divider">
|
||||
<label className="block text-sm font-medium text-page-label mb-3">{t('topup.cryptoPayment')}</label>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Chain */}
|
||||
<div className="flex gap-1.5">
|
||||
{availableChains.map((chain) => (
|
||||
<button
|
||||
key={chain.key}
|
||||
onClick={() => setSelectedChain(chain.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
selectedChain === chain.key
|
||||
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||
: 'glass-sm text-page-label hover:text-page hover:bg-page-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{chain.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Token */}
|
||||
<div className="flex gap-1.5">
|
||||
{['usdt', 'usdc'].map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
onClick={() => setSelectedToken(token)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
selectedToken === token
|
||||
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||
: 'glass-sm text-page-label hover:text-page hover:bg-page-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{token.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Pay */}
|
||||
<button
|
||||
onClick={handleCryptoPay}
|
||||
disabled={paymentLoading || !amount}
|
||||
className="px-4 py-2.5 rounded-xl text-sm font-medium glass-sm text-page-label hover:text-page hover:bg-page-surface-hover disabled:opacity-40 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{paymentLoading && payingMethod === 'crypto'
|
||||
? t('topup.processing')
|
||||
: `${selectedToken.toUpperCase()} (${availableChains.find(c => c.key === selectedChain)?.label || selectedChain})`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -526,81 +525,6 @@ export default function Topup() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Crypto Payment Section */}
|
||||
{site?.enable_topup && enableCrypto && availableChains.length > 0 && (
|
||||
<div className="glass rounded-2xl p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold text-page mb-4">{t('topup.cryptoPayment')}</h2>
|
||||
|
||||
{/* Amount input for crypto */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-page-label mb-2">{t('topup.amount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={amount ? Math.round(amount * rate) : ''}
|
||||
onChange={(e) => {
|
||||
const cnyVal = e.target.value;
|
||||
const usdVal = cnyVal ? Math.max(1, Math.ceil(cnyVal / rate)) : '';
|
||||
setAmount(usdVal);
|
||||
setSelectedPreset(null);
|
||||
}}
|
||||
min={Math.round(minTopup * rate)}
|
||||
placeholder={t('topup.amountPlaceholder', { min: Math.round(minTopup * rate) })}
|
||||
className="input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chain Selector */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-page-label mb-2">{t('topup.chain')}</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableChains.map((chain) => (
|
||||
<button
|
||||
key={chain.key}
|
||||
onClick={() => setSelectedChain(chain.key)}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
|
||||
selectedChain === chain.key
|
||||
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||
: 'glass-sm text-page-label hover:text-page hover:bg-page-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{chain.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Selector */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-page-label mb-2">{t('topup.token') || 'Token'}</label>
|
||||
<div className="flex gap-2">
|
||||
{['usdt', 'usdc'].map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
onClick={() => setSelectedToken(token)}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
|
||||
selectedToken === token
|
||||
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||
: 'glass-sm text-page-label hover:text-page hover:bg-page-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{token.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCryptoPay}
|
||||
disabled={paymentLoading || !amount}
|
||||
className="btn-primary w-full"
|
||||
>
|
||||
{paymentLoading && payingMethod === 'crypto'
|
||||
? t('topup.processing')
|
||||
: `${t('topup.payWith') || 'Pay with'} ${selectedToken.toUpperCase()} (${availableChains.find(c => c.key === selectedChain)?.label || selectedChain})`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Crypto Payment Modal */}
|
||||
{cryptoOrder && (
|
||||
<div className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setCryptoOrder(null)}>
|
||||
@@ -690,114 +614,6 @@ export default function Topup() {
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="glass-sm rounded-xl p-4 text-center">
|
||||
<p className="text-xs text-page-secondary mb-1">{t('topup.affAvailable') || '可用佣金'}</p>
|
||||
<p className="text-xl font-bold text-page">
|
||||
{symbol}{((user?.aff_quota || 0) / Q * rate).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-sm rounded-xl p-4 text-center">
|
||||
<p className="text-xs text-page-secondary mb-1">{t('topup.affTotal') || '累计佣金'}</p>
|
||||
<p className="text-xl font-bold text-page">
|
||||
{symbol}{((user?.aff_history_quota || 0) / Q * rate).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-sm rounded-xl p-4 text-center">
|
||||
<p className="text-xs text-page-secondary mb-1">{t('topup.affCount') || '邀请人数'}</p>
|
||||
<p className="text-xl font-bold text-page">{user?.aff_count || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invitation Link */}
|
||||
<div className="mb-5">
|
||||
<label className="block text-sm font-medium text-page-label mb-2">{t('topup.inviteLink') || '邀请链接'}</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={affLink}
|
||||
className="input flex-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyAffLink}
|
||||
className="btn-primary whitespace-nowrap text-sm px-4"
|
||||
>
|
||||
{t('topup.copy') || '复制'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={transferAmount}
|
||||
onChange={(e) => setTransferAmount(e.target.value)}
|
||||
placeholder={t('topup.transferPlaceholder') || '输入划转额度 (quota)'}
|
||||
className="input flex-1 text-sm"
|
||||
min={1}
|
||||
/>
|
||||
<button
|
||||
onClick={handleTransfer}
|
||||
disabled={transferring}
|
||||
className="btn-primary whitespace-nowrap text-sm px-4"
|
||||
>
|
||||
{transferring ? (t('topup.processing') || '处理中...') : (t('topup.transfer') || '划转')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Earnings Details */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => { setShowAffEarnings(!showAffEarnings); if (!showAffEarnings) loadAffEarnings(); }}
|
||||
className="text-sm text-page-secondary hover:text-page transition-colors"
|
||||
>
|
||||
{showAffEarnings ? (t('topup.hideEarnings') || '收起明细') : (t('topup.viewEarnings') || '查看收入明细')}
|
||||
</button>
|
||||
{showAffEarnings && (
|
||||
<div className="mt-3">
|
||||
{affEarningsLoading ? (
|
||||
<div className="flex justify-center py-6">
|
||||
<div className="w-6 h-6 border-2 border-brand-500/30 border-t-brand-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
) : affEarnings.length === 0 ? (
|
||||
<p className="text-sm text-page-muted text-center py-6">{t('topup.noEarnings') || '暂无收入记录'}</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{affEarnings.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between glass-sm rounded-xl px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm text-page">{item.model_name}</p>
|
||||
<p className="text-xs text-page-muted">
|
||||
{new Date(item.created_time * 1000).toLocaleString()} · {(item.commission_rate * 100).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-page-success">
|
||||
+{symbol}{(item.commission_quota / Q * rate).toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user