Commit 2ab2edec by t0ng7u

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)
parent ada604db
......@@ -36,7 +36,7 @@ const MjLogsActions = ({
const showSkeleton = useMinimumLoadingTime(loading);
const placeholder = (
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<div className="flex items-center mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
<Skeleton.Title style={{ width: 300, height: 21, borderRadius: 6 }} />
</div>
......@@ -45,7 +45,7 @@ const MjLogsActions = ({
return (
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<Skeleton loading={showSkeleton} active placeholder={placeholder}>
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<div className="flex items-center mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
<Text>
{isAdminUser && showBanner
......
......@@ -48,7 +48,7 @@ const InvitationCard = ({
</Avatar>
<div>
<Typography.Text className="text-lg font-medium">{t('邀请奖励')}</Typography.Text>
<div className="text-xs text-gray-600 dark:text-gray-400">{t('邀请好友获得额外奖励')}</div>
<div className="text-xs">{t('邀请好友获得额外奖励')}</div>
</div>
</div>
......
/*
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
*/
import React from 'react';
import { Card, Typography, Divider } from '@douyinfe/semi-ui';
import { Wallet, Coins, BarChart2 } from 'lucide-react';
const { Text } = Typography;
const RightStatsCard = ({ t, userState, renderQuota }) => {
return (
<Card size="small" className="!rounded-xl shadow-sm" bodyStyle={{ padding: '8px 12px' }}>
<div className="flex items-center gap-3 lg:gap-4 w-full">
<div className="flex items-center justify-end gap-2 flex-1">
<Wallet size={16} className="text-slate-600 dark:text-slate-300" />
<div className="text-right">
<Text size="small" type="tertiary">{t('当前余额')}</Text>
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.quota)}</div>
</div>
</div>
<Divider layout="vertical" className="hidden md:block" />
<div className="flex items-center justify-end gap-2 flex-1">
<Coins size={16} className="text-slate-600 dark:text-slate-300" />
<div className="text-right">
<Text size="small" type="tertiary">{t('历史消耗')}</Text>
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.used_quota)}</div>
</div>
</div>
<Divider layout="vertical" className="hidden md:block" />
<div className="flex items-center justify-end gap-2 flex-1">
<BarChart2 size={16} className="text-slate-600 dark:text-slate-300" />
<div className="text-right">
<Text size="small" type="tertiary">{t('请求次数')}</Text>
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{userState?.user?.request_count || 0}</div>
</div>
</div>
</div>
</Card>
);
};
export default RightStatsCard;
......@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useContext } from 'react';
import React, { useEffect, useState, useContext, useRef } from 'react';
import {
API,
showError,
......@@ -63,42 +63,25 @@ const TopUp = () => {
const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
const [statusLoading, setStatusLoading] = useState(true);
const [userQuota, setUserQuota] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false);
const [payWay, setPayWay] = useState('');
const [userDataLoading, setUserDataLoading] = useState(true);
const [amountLoading, setAmountLoading] = useState(false);
const [paymentLoading, setPaymentLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [payMethods, setPayMethods] = useState([]);
const affFetchedRef = useRef(false);
// 邀请相关状态
const [affLink, setAffLink] = useState('');
const [openTransfer, setOpenTransfer] = useState(false);
const [transferAmount, setTransferAmount] = useState(0);
// 预设充值额度选项
const [presetAmounts, setPresetAmounts] = useState([
{ value: 5 },
{ value: 10 },
{ value: 30 },
{ value: 50 },
{ value: 100 },
{ value: 300 },
{ value: 500 },
{ value: 1000 },
]);
const [presetAmounts, setPresetAmounts] = useState([]);
const [selectedPreset, setSelectedPreset] = useState(null);
const getUsername = () => {
if (userState.user) {
return userState.user.username;
} else {
return 'null';
}
};
const topUp = async () => {
if (redemptionCode === '') {
showInfo(t('请输入兑换码!'));
......@@ -117,9 +100,6 @@ const TopUp = () => {
content: t('成功兑换额度:') + renderQuota(data),
centered: true,
});
setUserQuota((quota) => {
return quota + data;
});
if (userState.user) {
const updatedUser = {
...userState.user,
......@@ -260,16 +240,13 @@ const TopUp = () => {
};
const getUserQuota = async () => {
setUserDataLoading(true);
let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data;
if (success) {
setUserQuota(data.quota);
userDispatch({ type: 'login', payload: data });
} else {
showError(message);
}
setUserDataLoading(false);
};
// 获取邀请链接
......@@ -310,13 +287,9 @@ const TopUp = () => {
};
useEffect(() => {
if (userState?.user?.id) {
setUserDataLoading(false);
setUserQuota(userState.user.quota);
} else {
if (!userState?.user?.id) {
getUserQuota().then();
}
getAffLink().then();
setTransferAmount(getQuotaPerUnit());
let payMethods = localStorage.getItem('pay_methods');
......@@ -330,9 +303,9 @@ const TopUp = () => {
// 如果没有color,则设置默认颜色
payMethods = payMethods.map((method) => {
if (!method.color) {
if (method.type === 'zfb') {
if (method.type === 'alipay') {
method.color = 'rgba(var(--semi-blue-5), 1)';
} else if (method.type === 'wx') {
} else if (method.type === 'wxpay') {
method.color = 'rgba(var(--semi-green-5), 1)';
} else if (method.type === 'stripe') {
method.color = 'rgba(var(--semi-purple-5), 1)';
......@@ -366,13 +339,26 @@ const TopUp = () => {
}, [statusState?.status?.enable_stripe_topup]);
useEffect(() => {
if (affFetchedRef.current) return;
affFetchedRef.current = true;
getAffLink().then();
}, []);
useEffect(() => {
if (statusState?.status) {
setMinTopUp(statusState.status.min_topup || 1);
setTopUpCount(statusState.status.min_topup || 1);
const minTopUpValue = statusState.status.min_topup || 1;
setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue);
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);
// 根据最小充值金额生成预设充值额度选项
setPresetAmounts(generatePresetAmounts(minTopUpValue));
// 初始化显示实付金额
getAmount(minTopUpValue);
setStatusLoading(false);
}
}, [statusState?.status]);
......@@ -454,6 +440,14 @@ const TopUp = () => {
return num.toString();
};
// 根据最小充值金额生成预设充值额度选项
const generatePresetAmounts = (minAmount) => {
const multipliers = [1, 5, 10, 30, 50, 100, 300, 500];
return multipliers.map(multiplier => ({
value: minAmount * multiplier
}));
};
return (
<div className='mx-auto relative min-h-screen lg:min-h-0 mt-[60px] px-2'>
{/* 划转模态框 */}
......
......@@ -85,9 +85,9 @@ const PaymentConfirmModal = ({
if (payMethod) {
return (
<>
{payMethod.type === 'zfb' ? (
{payMethod.type === 'alipay' ? (
<SiAlipay className='mr-2' size={16} color="#1677FF" />
) : payMethod.type === 'wx' ? (
) : payMethod.type === 'wxpay' ? (
<SiWechat className='mr-2' size={16} color="#07C160" />
) : payMethod.type === 'stripe' ? (
<SiStripe className='mr-2' size={16} color="#635BFF" />
......@@ -99,7 +99,7 @@ const PaymentConfirmModal = ({
);
} else {
// 默认充值方式
if (payWay === 'zfb') {
if (payWay === 'alipay') {
return (
<>
<SiAlipay className='mr-2' size={16} color="#1677FF" />
......
......@@ -159,7 +159,6 @@
"测试所有已启用通道": "Test all enabled channels",
"更新所有已启用通道余额": "Update balance for all enabled channels",
"刷新": "Refresh",
"处理中...": "Processing...",
"绑定成功!": "Binding successful!",
"登录成功!": "Login successful!",
"操作失败,重定向至登录界面中...": "Operation failed, redirecting to login page...",
......@@ -168,7 +167,7 @@
"渠道": "Channel",
"渠道管理": "Channels",
"令牌": "Tokens",
"兑换": "Redeem",
"兑换额度": "Redeem",
"充值": "Recharge",
"用户": "Users",
"日志": "Logs",
......@@ -846,7 +845,6 @@
"充值记录": "Recharge record",
"返利记录": "Rebate record",
"确定要充值 $": "Confirm to top up $",
"兑换中...": "Redemming",
"微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:",
"Stripe 实付金额:": "Stripe actual payment amount:",
"支付中...": "Paying",
......@@ -857,7 +855,9 @@
"兑换码充值": "Redemption code recharge",
"奖励说明": "Reward description",
"选择支付方式": "Select payment method",
"处理中": "Processing",
"在找兑换码?": "Looking for a redemption code? ",
"购买兑换码": "Buy redemption code",
"账户统计": "Account statistics",
"账户充值": "Account recharge",
"多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient",
"支付方式": "Payment method",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment