2025-08-17 04:00:58 +08:00
|
|
|
|
/*
|
|
|
|
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
|
|
it under the terms of the GNU Affero General Public License as
|
|
|
|
|
|
published by the Free Software Foundation, either version 3 of the
|
|
|
|
|
|
License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
import React, { useEffect, useState, useContext, useRef } from 'react';
|
2025-08-17 04:00:58 +08:00
|
|
|
|
import {
|
|
|
|
|
|
API,
|
|
|
|
|
|
showError,
|
|
|
|
|
|
showInfo,
|
|
|
|
|
|
showSuccess,
|
|
|
|
|
|
renderQuota,
|
|
|
|
|
|
renderQuotaWithAmount,
|
|
|
|
|
|
copy,
|
|
|
|
|
|
getQuotaPerUnit,
|
|
|
|
|
|
} from '../../helpers';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Toast,
|
|
|
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
import { UserContext } from '../../context/User';
|
2025-08-18 04:14:35 +08:00
|
|
|
|
import { StatusContext } from '../../context/Status';
|
2025-08-17 04:00:58 +08:00
|
|
|
|
|
|
|
|
|
|
import RechargeCard from './RechargeCard';
|
|
|
|
|
|
import InvitationCard from './InvitationCard';
|
|
|
|
|
|
import TransferModal from './modals/TransferModal';
|
|
|
|
|
|
import PaymentConfirmModal from './modals/PaymentConfirmModal';
|
|
|
|
|
|
|
|
|
|
|
|
const TopUp = () => {
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
const [userState, userDispatch] = useContext(UserContext);
|
|
|
|
|
|
const [statusState] = useContext(StatusContext);
|
|
|
|
|
|
|
|
|
|
|
|
const [redemptionCode, setRedemptionCode] = useState('');
|
|
|
|
|
|
const [amount, setAmount] = useState(0.0);
|
|
|
|
|
|
const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1);
|
|
|
|
|
|
const [topUpCount, setTopUpCount] = useState(
|
|
|
|
|
|
statusState?.status?.min_topup || 1,
|
|
|
|
|
|
);
|
|
|
|
|
|
const [topUpLink, setTopUpLink] = useState(
|
|
|
|
|
|
statusState?.status?.top_up_link || '',
|
|
|
|
|
|
);
|
|
|
|
|
|
const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(
|
|
|
|
|
|
statusState?.status?.enable_online_topup || false,
|
|
|
|
|
|
);
|
|
|
|
|
|
const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1);
|
|
|
|
|
|
|
|
|
|
|
|
const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
|
|
|
|
|
|
const [statusLoading, setStatusLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
|
const [payWay, setPayWay] = useState('');
|
|
|
|
|
|
const [amountLoading, setAmountLoading] = useState(false);
|
|
|
|
|
|
const [paymentLoading, setPaymentLoading] = useState(false);
|
|
|
|
|
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
|
|
|
|
|
const [payMethods, setPayMethods] = useState([]);
|
|
|
|
|
|
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
const affFetchedRef = useRef(false);
|
|
|
|
|
|
|
2025-08-17 04:00:58 +08:00
|
|
|
|
// 邀请相关状态
|
|
|
|
|
|
const [affLink, setAffLink] = useState('');
|
|
|
|
|
|
const [openTransfer, setOpenTransfer] = useState(false);
|
|
|
|
|
|
const [transferAmount, setTransferAmount] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 预设充值额度选项
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
const [presetAmounts, setPresetAmounts] = useState([]);
|
2025-08-17 04:00:58 +08:00
|
|
|
|
const [selectedPreset, setSelectedPreset] = useState(null);
|
|
|
|
|
|
|
|
|
|
|
|
const topUp = async () => {
|
|
|
|
|
|
if (redemptionCode === '') {
|
|
|
|
|
|
showInfo(t('请输入兑换码!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setIsSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.post('/api/user/topup', {
|
|
|
|
|
|
key: redemptionCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
const { success, message, data } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('兑换成功!'));
|
|
|
|
|
|
Modal.success({
|
|
|
|
|
|
title: t('兑换成功!'),
|
|
|
|
|
|
content: t('成功兑换额度:') + renderQuota(data),
|
|
|
|
|
|
centered: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (userState.user) {
|
|
|
|
|
|
const updatedUser = {
|
|
|
|
|
|
...userState.user,
|
|
|
|
|
|
quota: userState.user.quota + data,
|
|
|
|
|
|
};
|
|
|
|
|
|
userDispatch({ type: 'login', payload: updatedUser });
|
|
|
|
|
|
}
|
|
|
|
|
|
setRedemptionCode('');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showError(t('请求失败'));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openTopUpLink = () => {
|
|
|
|
|
|
if (!topUpLink) {
|
|
|
|
|
|
showError(t('超级管理员未设置充值链接!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
window.open(topUpLink, '_blank');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const preTopUp = async (payment) => {
|
|
|
|
|
|
if (payment === 'stripe') {
|
|
|
|
|
|
if (!enableStripeTopUp) {
|
|
|
|
|
|
showError(t('管理员未开启Stripe充值!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!enableOnlineTopUp) {
|
|
|
|
|
|
showError(t('管理员未开启在线充值!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setPayWay(payment);
|
|
|
|
|
|
setPaymentLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (payment === 'stripe') {
|
|
|
|
|
|
await getStripeAmount();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await getAmount();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (topUpCount < minTopUp) {
|
|
|
|
|
|
showError(t('充值数量不能小于') + minTopUp);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setOpen(true);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
showError(t('获取金额失败'));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setPaymentLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onlineTopUp = async () => {
|
|
|
|
|
|
if (payWay === 'stripe') {
|
|
|
|
|
|
// Stripe 支付处理
|
|
|
|
|
|
if (amount === 0) {
|
|
|
|
|
|
await getStripeAmount();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 普通支付处理
|
|
|
|
|
|
if (amount === 0) {
|
|
|
|
|
|
await getAmount();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (topUpCount < minTopUp) {
|
|
|
|
|
|
showError('充值数量不能小于' + minTopUp);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setConfirmLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res;
|
|
|
|
|
|
if (payWay === 'stripe') {
|
|
|
|
|
|
// Stripe 支付请求
|
|
|
|
|
|
res = await API.post('/api/user/stripe/pay', {
|
|
|
|
|
|
amount: parseInt(topUpCount),
|
|
|
|
|
|
payment_method: 'stripe',
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 普通支付请求
|
|
|
|
|
|
res = await API.post('/api/user/pay', {
|
|
|
|
|
|
amount: parseInt(topUpCount),
|
|
|
|
|
|
payment_method: payWay,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (res !== undefined) {
|
|
|
|
|
|
const { message, data } = res.data;
|
|
|
|
|
|
if (message === 'success') {
|
|
|
|
|
|
if (payWay === 'stripe') {
|
|
|
|
|
|
// Stripe 支付回调处理
|
|
|
|
|
|
window.open(data.pay_link, '_blank');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 普通支付表单提交
|
|
|
|
|
|
let params = data;
|
|
|
|
|
|
let url = res.data.url;
|
|
|
|
|
|
let form = document.createElement('form');
|
|
|
|
|
|
form.action = url;
|
|
|
|
|
|
form.method = 'POST';
|
|
|
|
|
|
let isSafari =
|
|
|
|
|
|
navigator.userAgent.indexOf('Safari') > -1 &&
|
|
|
|
|
|
navigator.userAgent.indexOf('Chrome') < 1;
|
|
|
|
|
|
if (!isSafari) {
|
|
|
|
|
|
form.target = '_blank';
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let key in params) {
|
|
|
|
|
|
let input = document.createElement('input');
|
|
|
|
|
|
input.type = 'hidden';
|
|
|
|
|
|
input.name = key;
|
|
|
|
|
|
input.value = params[key];
|
|
|
|
|
|
form.appendChild(input);
|
|
|
|
|
|
}
|
|
|
|
|
|
document.body.appendChild(form);
|
|
|
|
|
|
form.submit();
|
|
|
|
|
|
document.body.removeChild(form);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(res);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.log(err);
|
|
|
|
|
|
showError(t('支付请求失败'));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setOpen(false);
|
|
|
|
|
|
setConfirmLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getUserQuota = async () => {
|
|
|
|
|
|
let res = await API.get(`/api/user/self`);
|
|
|
|
|
|
const { success, message, data } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
userDispatch({ type: 'login', payload: data });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取邀请链接
|
|
|
|
|
|
const getAffLink = async () => {
|
|
|
|
|
|
const res = await API.get('/api/user/aff');
|
|
|
|
|
|
const { success, message, data } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
let link = `${window.location.origin}/register?aff=${data}`;
|
|
|
|
|
|
setAffLink(link);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 划转邀请额度
|
|
|
|
|
|
const transfer = async () => {
|
|
|
|
|
|
if (transferAmount < getQuotaPerUnit()) {
|
|
|
|
|
|
showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await API.post(`/api/user/aff_transfer`, {
|
|
|
|
|
|
quota: transferAmount,
|
|
|
|
|
|
});
|
|
|
|
|
|
const { success, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(message);
|
|
|
|
|
|
setOpenTransfer(false);
|
|
|
|
|
|
getUserQuota().then();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 复制邀请链接
|
|
|
|
|
|
const handleAffLinkClick = async () => {
|
|
|
|
|
|
await copy(affLink);
|
|
|
|
|
|
showSuccess(t('邀请链接已复制到剪切板'));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
if (!userState?.user?.id) {
|
2025-08-17 04:00:58 +08:00
|
|
|
|
getUserQuota().then();
|
|
|
|
|
|
}
|
|
|
|
|
|
setTransferAmount(getQuotaPerUnit());
|
|
|
|
|
|
|
|
|
|
|
|
let payMethods = localStorage.getItem('pay_methods');
|
|
|
|
|
|
try {
|
|
|
|
|
|
payMethods = JSON.parse(payMethods);
|
|
|
|
|
|
if (payMethods && payMethods.length > 0) {
|
|
|
|
|
|
// 检查name和type是否为空
|
|
|
|
|
|
payMethods = payMethods.filter((method) => {
|
|
|
|
|
|
return method.name && method.type;
|
|
|
|
|
|
});
|
|
|
|
|
|
// 如果没有color,则设置默认颜色
|
|
|
|
|
|
payMethods = payMethods.map((method) => {
|
|
|
|
|
|
if (!method.color) {
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
if (method.type === 'alipay') {
|
2025-08-17 04:00:58 +08:00
|
|
|
|
method.color = 'rgba(var(--semi-blue-5), 1)';
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
} else if (method.type === 'wxpay') {
|
2025-08-17 04:00:58 +08:00
|
|
|
|
method.color = 'rgba(var(--semi-green-5), 1)';
|
|
|
|
|
|
} else if (method.type === 'stripe') {
|
|
|
|
|
|
method.color = 'rgba(var(--semi-purple-5), 1)';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
method.color = 'rgba(var(--semi-primary-5), 1)';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return method;
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payMethods = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果启用了 Stripe 支付,添加到支付方法列表
|
|
|
|
|
|
if (statusState?.status?.enable_stripe_topup) {
|
|
|
|
|
|
const hasStripe = payMethods.some(method => method.type === 'stripe');
|
|
|
|
|
|
if (!hasStripe) {
|
|
|
|
|
|
payMethods.push({
|
|
|
|
|
|
name: 'Stripe',
|
|
|
|
|
|
type: 'stripe',
|
|
|
|
|
|
color: 'rgba(var(--semi-purple-5), 1)'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setPayMethods(payMethods);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log(e);
|
|
|
|
|
|
showError(t('支付方式配置错误, 请联系管理员'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [statusState?.status?.enable_stripe_topup]);
|
|
|
|
|
|
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (affFetchedRef.current) return;
|
|
|
|
|
|
affFetchedRef.current = true;
|
|
|
|
|
|
getAffLink().then();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-08-17 04:00:58 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (statusState?.status) {
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
const minTopUpValue = statusState.status.min_topup || 1;
|
|
|
|
|
|
setMinTopUp(minTopUpValue);
|
|
|
|
|
|
setTopUpCount(minTopUpValue);
|
2025-08-17 04:00:58 +08:00
|
|
|
|
setTopUpLink(statusState.status.top_up_link || '');
|
|
|
|
|
|
setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
|
|
|
|
|
|
setPriceRatio(statusState.status.price || 1);
|
|
|
|
|
|
setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据最小充值金额生成预设充值额度选项
|
|
|
|
|
|
setPresetAmounts(generatePresetAmounts(minTopUpValue));
|
|
|
|
|
|
// 初始化显示实付金额
|
|
|
|
|
|
getAmount(minTopUpValue);
|
|
|
|
|
|
|
2025-08-17 04:00:58 +08:00
|
|
|
|
setStatusLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [statusState?.status]);
|
|
|
|
|
|
|
|
|
|
|
|
const renderAmount = () => {
|
|
|
|
|
|
return amount + ' ' + t('元');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getAmount = async (value) => {
|
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
|
value = topUpCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
setAmountLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.post('/api/user/amount', {
|
|
|
|
|
|
amount: parseFloat(value),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res !== undefined) {
|
|
|
|
|
|
const { message, data } = res.data;
|
|
|
|
|
|
if (message === 'success') {
|
|
|
|
|
|
setAmount(parseFloat(data));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setAmount(0);
|
|
|
|
|
|
Toast.error({ content: '错误:' + data, id: 'getAmount' });
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(res);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.log(err);
|
|
|
|
|
|
}
|
|
|
|
|
|
setAmountLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getStripeAmount = async (value) => {
|
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
|
value = topUpCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
setAmountLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.post('/api/user/stripe/amount', {
|
|
|
|
|
|
amount: parseFloat(value),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res !== undefined) {
|
|
|
|
|
|
const { message, data } = res.data;
|
|
|
|
|
|
if (message === 'success') {
|
|
|
|
|
|
setAmount(parseFloat(data));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setAmount(0);
|
|
|
|
|
|
Toast.error({ content: '错误:' + data, id: 'getAmount' });
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(res);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.log(err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setAmountLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTransferCancel = () => {
|
|
|
|
|
|
setOpenTransfer(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 选择预设充值额度
|
|
|
|
|
|
const selectPresetAmount = (preset) => {
|
|
|
|
|
|
setTopUpCount(preset.value);
|
|
|
|
|
|
setSelectedPreset(preset.value);
|
|
|
|
|
|
setAmount(preset.value * priceRatio);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化大数字显示
|
|
|
|
|
|
const formatLargeNumber = (num) => {
|
|
|
|
|
|
return num.toString();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests
- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
- Use Button icon prop for Alipay/WeChat/Stripe with brand colors
- Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)
TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors
Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)
Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)
Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
2025-08-23 01:32:54 +08:00
|
|
|
|
// 根据最小充值金额生成预设充值额度选项
|
|
|
|
|
|
const generatePresetAmounts = (minAmount) => {
|
|
|
|
|
|
const multipliers = [1, 5, 10, 30, 50, 100, 300, 500];
|
|
|
|
|
|
return multipliers.map(multiplier => ({
|
|
|
|
|
|
value: minAmount * multiplier
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-17 04:00:58 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className='mx-auto relative min-h-screen lg:min-h-0 mt-[60px] px-2'>
|
|
|
|
|
|
{/* 划转模态框 */}
|
|
|
|
|
|
<TransferModal
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
openTransfer={openTransfer}
|
|
|
|
|
|
transfer={transfer}
|
|
|
|
|
|
handleTransferCancel={handleTransferCancel}
|
|
|
|
|
|
userState={userState}
|
|
|
|
|
|
renderQuota={renderQuota}
|
|
|
|
|
|
getQuotaPerUnit={getQuotaPerUnit}
|
|
|
|
|
|
transferAmount={transferAmount}
|
|
|
|
|
|
setTransferAmount={setTransferAmount}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 充值确认模态框 */}
|
|
|
|
|
|
<PaymentConfirmModal
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
onlineTopUp={onlineTopUp}
|
|
|
|
|
|
handleCancel={handleCancel}
|
|
|
|
|
|
confirmLoading={confirmLoading}
|
|
|
|
|
|
topUpCount={topUpCount}
|
|
|
|
|
|
renderQuotaWithAmount={renderQuotaWithAmount}
|
|
|
|
|
|
amountLoading={amountLoading}
|
|
|
|
|
|
renderAmount={renderAmount}
|
|
|
|
|
|
payWay={payWay}
|
|
|
|
|
|
payMethods={payMethods}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 用户信息头部 */}
|
|
|
|
|
|
<div className='space-y-6'>
|
|
|
|
|
|
<div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
|
|
|
|
|
|
{/* 左侧充值区域 */}
|
|
|
|
|
|
<div className='lg:col-span-7 space-y-6 w-full'>
|
|
|
|
|
|
<RechargeCard
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
enableOnlineTopUp={enableOnlineTopUp}
|
|
|
|
|
|
enableStripeTopUp={enableStripeTopUp}
|
|
|
|
|
|
presetAmounts={presetAmounts}
|
|
|
|
|
|
selectedPreset={selectedPreset}
|
|
|
|
|
|
selectPresetAmount={selectPresetAmount}
|
|
|
|
|
|
formatLargeNumber={formatLargeNumber}
|
|
|
|
|
|
priceRatio={priceRatio}
|
|
|
|
|
|
topUpCount={topUpCount}
|
|
|
|
|
|
minTopUp={minTopUp}
|
|
|
|
|
|
renderQuotaWithAmount={renderQuotaWithAmount}
|
|
|
|
|
|
getAmount={getAmount}
|
|
|
|
|
|
setTopUpCount={setTopUpCount}
|
|
|
|
|
|
setSelectedPreset={setSelectedPreset}
|
|
|
|
|
|
renderAmount={renderAmount}
|
|
|
|
|
|
amountLoading={amountLoading}
|
|
|
|
|
|
payMethods={payMethods}
|
|
|
|
|
|
preTopUp={preTopUp}
|
|
|
|
|
|
paymentLoading={paymentLoading}
|
|
|
|
|
|
payWay={payWay}
|
|
|
|
|
|
redemptionCode={redemptionCode}
|
|
|
|
|
|
setRedemptionCode={setRedemptionCode}
|
|
|
|
|
|
topUp={topUp}
|
|
|
|
|
|
isSubmitting={isSubmitting}
|
|
|
|
|
|
topUpLink={topUpLink}
|
|
|
|
|
|
openTopUpLink={openTopUpLink}
|
|
|
|
|
|
userState={userState}
|
|
|
|
|
|
renderQuota={renderQuota}
|
|
|
|
|
|
statusLoading={statusLoading}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 右侧信息区域 */}
|
|
|
|
|
|
<div className='lg:col-span-5'>
|
|
|
|
|
|
<InvitationCard
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
userState={userState}
|
|
|
|
|
|
renderQuota={renderQuota}
|
|
|
|
|
|
setOpenTransfer={setOpenTransfer}
|
|
|
|
|
|
affLink={affLink}
|
|
|
|
|
|
handleAffLinkClick={handleAffLinkClick}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default TopUp;
|