From 3749be3e09dd896c32ba4da5853613a9c49f2ffb Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 17 Aug 2025 04:00:58 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=B3=20feat(TopUp):=20unify=20payment?= =?UTF-8?q?=20cards,=20add=20header=20stats,=20brand=20icons,=20and=20mobi?= =?UTF-8?q?le=20refinements=20[[memory:5659506]]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RightStatsCard and place it in RechargeCard header - Shows current balance, historical spend, and request count - Mobile: stacks under title; three metrics split equally (flex-1); vertical dividers hidden on small screens - Remove extra margins; small card styling - RechargeCard - Replace redeem code Input icon with Semi UI IconGift - Style “Payable amount” number in red and bold; keep same style in confirm modal - Always render payment methods as Cards (remove Button variant) with adaptive grid - Use brand color icons: SiAlipay (#1677FF), SiWechat (#07C160), SiStripe (#635BFF) - Replace Stripe icon with SiStripe - Integrate RightStatsCard props; adjust header to flex-col on mobile and flex-row on desktop - Hide Banner close button when online top-up is disabled (closeIcon={null}) - InvitationCard - Simplify to match RechargeCard’s minimalist slate style - Use Card title for “Rewards” and place content directly in body - Improve link input and copy button; use Badge dots for bullet points - TopUp index - Remove separate right-column stats card; pass userState and renderQuota to RechargeCard - Cleanup - Lint passes; no functional changes to APIs or business logic --- web/src/components/common/ui/CardPro.js | 4 +- web/src/components/dashboard/ChartsPanel.jsx | 2 +- web/src/components/layout/HeaderBar.js | 2 +- web/src/components/layout/SiderBar.js | 20 +- .../playground/CustomRequestEditor.js | 2 +- .../components/settings/PersonalSetting.js | 2 +- .../personal/components/UserInfoHeader.js | 2 +- web/src/components/topup/InvitationCard.jsx | 164 +++ web/src/components/topup/RechargeCard.jsx | 305 ++++ web/src/components/topup/RightStatsCard.jsx | 60 + web/src/components/topup/index.jsx | 542 +++++++ .../topup/modals/PaymentConfirmModal.jsx | 133 ++ .../components/topup/modals/TransferModal.jsx | 82 ++ web/src/constants/dashboard.constants.js | 4 +- web/src/i18n/locales/en.json | 12 +- web/src/pages/TopUp/index.js | 1310 +---------------- 16 files changed, 1315 insertions(+), 1331 deletions(-) create mode 100644 web/src/components/topup/InvitationCard.jsx create mode 100644 web/src/components/topup/RechargeCard.jsx create mode 100644 web/src/components/topup/RightStatsCard.jsx create mode 100644 web/src/components/topup/index.jsx create mode 100644 web/src/components/topup/modals/PaymentConfirmModal.jsx create mode 100644 web/src/components/topup/modals/TransferModal.jsx diff --git a/web/src/components/common/ui/CardPro.js b/web/src/components/common/ui/CardPro.js index ad6dda85..3e124722 100644 --- a/web/src/components/common/ui/CardPro.js +++ b/web/src/components/common/ui/CardPro.js @@ -53,8 +53,8 @@ const CardPro = ({ searchArea, paginationArea, // 新增分页区域 // 卡片属性 - shadows = 'always', - bordered = false, + shadows = '', + bordered = true, // 自定义样式 style, // 国际化函数 diff --git a/web/src/components/dashboard/ChartsPanel.jsx b/web/src/components/dashboard/ChartsPanel.jsx index 9eaab07e..26a43b63 100644 --- a/web/src/components/dashboard/ChartsPanel.jsx +++ b/web/src/components/dashboard/ChartsPanel.jsx @@ -45,7 +45,7 @@ const ChartsPanel = ({ return (
diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js index 61f82ba2..ffb5fc6e 100644 --- a/web/src/components/layout/HeaderBar.js +++ b/web/src/components/layout/HeaderBar.js @@ -367,7 +367,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { >
- {t('钱包')} + {t('钱包管理')}
diff --git a/web/src/components/layout/SiderBar.js b/web/src/components/layout/SiderBar.js index 985042ee..2e8829be 100644 --- a/web/src/components/layout/SiderBar.js +++ b/web/src/components/layout/SiderBar.js @@ -113,7 +113,7 @@ const SiderBar = ({ onNavigate = () => { } }) => { const financeItems = useMemo( () => [ { - text: t('钱包'), + text: t('钱包管理'), itemKey: 'topup', to: '/topup', }, @@ -397,6 +397,15 @@ const SiderBar = ({ onNavigate = () => { } }) => { {workspaceItems.map((item) => renderNavItem(item))}
+ {/* 个人中心区域 */} + +
+ {!collapsed && ( +
{t('个人中心')}
+ )} + {financeItems.map((item) => renderNavItem(item))} +
+ {/* 管理员区域 - 只在管理员时显示 */} {isAdmin() && ( <> @@ -409,15 +418,6 @@ const SiderBar = ({ onNavigate = () => { } }) => { )} - - {/* 个人中心区域 */} - -
- {!collapsed && ( -
{t('个人中心')}
- )} - {financeItems.map((item) => renderNavItem(item))} -
{/* 底部折叠按钮 */} diff --git a/web/src/components/playground/CustomRequestEditor.js b/web/src/components/playground/CustomRequestEditor.js index cd21398a..e411d9e7 100644 --- a/web/src/components/playground/CustomRequestEditor.js +++ b/web/src/components/playground/CustomRequestEditor.js @@ -139,7 +139,7 @@ const CustomRequestEditor = ({ description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。" icon={} className="!rounded-lg" - closable={false} + closeIcon={null} /> {/* JSON编辑器 */} diff --git a/web/src/components/settings/PersonalSetting.js b/web/src/components/settings/PersonalSetting.js index 75401c1c..0c98509e 100644 --- a/web/src/components/settings/PersonalSetting.js +++ b/web/src/components/settings/PersonalSetting.js @@ -328,7 +328,7 @@ const PersonalSetting = () => { return (
-
+
{/* 顶部用户信息区域 */} diff --git a/web/src/components/settings/personal/components/UserInfoHeader.js b/web/src/components/settings/personal/components/UserInfoHeader.js index dab2d1fe..f128d7d1 100644 --- a/web/src/components/settings/personal/components/UserInfoHeader.js +++ b/web/src/components/settings/personal/components/UserInfoHeader.js @@ -144,7 +144,7 @@ const UserInfoHeader = ({ t, userState }) => { {/* 移动端统计信息卡片(仅 xs 可见) */}
- +
diff --git a/web/src/components/topup/InvitationCard.jsx b/web/src/components/topup/InvitationCard.jsx new file mode 100644 index 00000000..65d46952 --- /dev/null +++ b/web/src/components/topup/InvitationCard.jsx @@ -0,0 +1,164 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { + Avatar, + Typography, + Card, + Button, + Input, + Badge, +} from '@douyinfe/semi-ui'; +import { Copy, Users, BarChart2, TrendingUp, Gift, Zap } from 'lucide-react'; + +const { Text } = Typography; + +const InvitationCard = ({ + t, + userState, + renderQuota, + setOpenTransfer, + affLink, + handleAffLinkClick, +}) => { + return ( + + {/* 卡片头部 */} +
+ + + +
+ {t('邀请奖励')} +
{t('邀请好友获得额外奖励')}
+
+
+ + {/* 收益展示区域 */} +
+ {/* 主要收益卡片 - 待使用收益 */} + +
+
+ + {t('待使用收益')} +
+ +
+
+ {renderQuota(userState?.user?.aff_quota || 0)} +
+
{t('可随时划转到账户余额')}
+
+ + {/* 统计数据网格 */} +
+ +
+ + {t('总收益')} +
+
+ {renderQuota(userState?.user?.aff_history_quota || 0)} +
+
{t('累计获得')}
+
+ + +
+ + {t('邀请人数')} +
+
+ {userState?.user?.aff_count || 0} {t('人')} +
+
{t('成功邀请')}
+
+
+ +
+ {/* 邀请链接部分 */} + } + className='!rounded-lg' + > + {t('复制')} + + } + /> +
+ + {/* 奖励说明 */} + + {t('奖励说明')} + + } + > +
+
+ + + {t('邀请好友注册,好友充值后您可获得相应奖励')} + +
+ +
+ + + {t('通过划转功能将奖励额度转入到您的账户余额中')} + +
+ +
+ + + {t('邀请的好友越多,获得的奖励越多')} + +
+
+
+
+
+ ); +}; + +export default InvitationCard; diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx new file mode 100644 index 00000000..9491d705 --- /dev/null +++ b/web/src/components/topup/RechargeCard.jsx @@ -0,0 +1,305 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { + Avatar, + Typography, + Card, + Button, + Input, + InputNumber, + Banner, + Skeleton, + Divider, + Tabs, + TabPane, +} from '@douyinfe/semi-ui'; +import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; +import { CreditCard, Gift, Link as LinkIcon, Coins } from 'lucide-react'; +import { IconGift } from '@douyinfe/semi-icons'; +import RightStatsCard from './RightStatsCard'; + +const { Text } = Typography; + +const RechargeCard = ({ + t, + enableOnlineTopUp, + enableStripeTopUp, + presetAmounts, + selectedPreset, + selectPresetAmount, + formatLargeNumber, + priceRatio, + topUpCount, + minTopUp, + renderQuotaWithAmount, + getAmount, + setTopUpCount, + setSelectedPreset, + renderAmount, + amountLoading, + payMethods, + preTopUp, + paymentLoading, + payWay, + redemptionCode, + setRedemptionCode, + topUp, + isSubmitting, + topUpLink, + openTopUpLink, + // 新增:用于右侧统计卡片 + userState, + renderQuota, + statusLoading, +}) => { + return ( + + {/* 卡片头部 */} +
+
+ + + +
+ {t('账户充值')} +
{t('多种充值方式,安全便捷')}
+
+
+ +
+ + + {/* 在线充值 Tab */} + + + {t('在线充值')} +
+ } + itemKey="online" + > +
+ {statusLoading ? ( +
+
+
+ ) : (enableOnlineTopUp || enableStripeTopUp) ? ( +
+ {/* 预设充值额度选择 */} + {(enableOnlineTopUp || enableStripeTopUp) && ( +
+ + {t('选择充值额度')} + +
+ {presetAmounts.map((preset, index) => ( + selectPresetAmount(preset)} + className={`cursor-pointer !rounded-xl transition-all hover:shadow-md ${selectedPreset === preset.value + ? 'border-blue-500 shadow-md' + : 'border-slate-200 hover:border-slate-300 dark:border-slate-600 dark:hover:border-slate-500' + }`} + bodyStyle={{ textAlign: 'center', padding: '12px' }} + > +
+ + {formatLargeNumber(preset.value)} +
+
+ {t('实付')} ¥{(preset.value * priceRatio).toFixed(2)} +
+
+ ))} +
+
+ )} + + {/* 自定义充值金额 */} + {(enableOnlineTopUp || enableStripeTopUp) && ( +
+ + + {t('或输入自定义金额')} + + + +
+
+ {t('充值数量')} + {amountLoading ? ( + + ) : ( + + {t('实付金额:')}{renderAmount()} + + )} +
+ { + if (value && value >= 1) { + setTopUpCount(value); + setSelectedPreset(null); + await getAmount(value); + } + }} + onBlur={(e) => { + const value = parseInt(e.target.value); + if (!value || value < 1) { + setTopUpCount(1); + getAmount(1); + } + }} + className='w-full !rounded-lg' + formatter={(value) => (value ? `${value}` : '')} + parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0} + /> +
+ + {/* 支付方式选择 */} +
+ + {t('选择支付方式')} + +
+ {payMethods.map((payMethod) => ( + preTopUp(payMethod.type)} + className={`cursor-pointer !rounded-xl transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type + ? 'border-blue-500 shadow-md' + : 'border-slate-200 hover:border-slate-300 dark:border-slate-600 dark:hover:border-slate-500' + } ${(!enableOnlineTopUp && payMethod.type !== 'stripe') || + (!enableStripeTopUp && payMethod.type === 'stripe') + ? 'opacity-50 cursor-not-allowed' + : '' + }`} + bodyStyle={{ padding: '12px', textAlign: 'center' }} + > + {paymentLoading && payWay === payMethod.type ? ( +
+
+
+
+
{t('处理中')}
+
+ ) : ( + <> +
+ {payMethod.type === 'zfb' ? ( + + ) : payMethod.type === 'wx' ? ( + + ) : payMethod.type === 'stripe' ? ( + + ) : ( + + )} +
+
{payMethod.name}
+ + )} +
+ ))} +
+
+
+ )} + +
+ ) : ( +
+ +
+ )} +
+ + + {/* 兑换码充值 Tab */} + + + {t('兑换码充值')} +
+ } + itemKey="redeem" + > +
+
+ setRedemptionCode(value)} + className='!rounded-lg' + prefix={} + /> + +
+ {topUpLink && ( + + )} + +
+
+
+ + + + ); +}; + +export default RechargeCard; diff --git a/web/src/components/topup/RightStatsCard.jsx b/web/src/components/topup/RightStatsCard.jsx new file mode 100644 index 00000000..8daa8d35 --- /dev/null +++ b/web/src/components/topup/RightStatsCard.jsx @@ -0,0 +1,60 @@ +/* +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 . + +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 ( + +
+
+ +
+ {t('当前余额')} +
{renderQuota(userState?.user?.quota)}
+
+
+ +
+ +
+ {t('历史消耗')} +
{renderQuota(userState?.user?.used_quota)}
+
+
+ +
+ +
+ {t('请求次数')} +
{userState?.user?.request_count || 0}
+
+
+
+
+ ); +}; + +export default RightStatsCard; + + diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx new file mode 100644 index 00000000..f96a459f --- /dev/null +++ b/web/src/components/topup/index.jsx @@ -0,0 +1,542 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState, useContext } from 'react'; +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'; +import { StatusContext } from '../../context/Status/index.js'; + +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 [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 [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 [selectedPreset, setSelectedPreset] = useState(null); + + const getUsername = () => { + if (userState.user) { + return userState.user.username; + } else { + return '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, + }); + setUserQuota((quota) => { + return quota + data; + }); + 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 () => { + 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); + }; + + // 获取邀请链接 + 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(() => { + if (userState?.user?.id) { + setUserDataLoading(false); + setUserQuota(userState.user.quota); + } else { + getUserQuota().then(); + } + getAffLink().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) { + if (method.type === 'zfb') { + method.color = 'rgba(var(--semi-blue-5), 1)'; + } else if (method.type === 'wx') { + 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]); + + useEffect(() => { + if (statusState?.status) { + setMinTopUp(statusState.status.min_topup || 1); + setTopUpCount(statusState.status.min_topup || 1); + 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); + 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(); + }; + + return ( +
+ {/* 划转模态框 */} + + + {/* 充值确认模态框 */} + + + {/* 用户信息头部 */} +
+
+ {/* 左侧充值区域 */} +
+ +
+ + {/* 右侧信息区域 */} +
+ +
+
+
+
+ ); +}; + +export default TopUp; diff --git a/web/src/components/topup/modals/PaymentConfirmModal.jsx b/web/src/components/topup/modals/PaymentConfirmModal.jsx new file mode 100644 index 00000000..e81199ca --- /dev/null +++ b/web/src/components/topup/modals/PaymentConfirmModal.jsx @@ -0,0 +1,133 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { + Modal, + Typography, + Card, + Skeleton, +} from '@douyinfe/semi-ui'; +import { SiAlipay, SiWechat } from 'react-icons/si'; +import { CreditCard } from 'lucide-react'; + +const { Text } = Typography; + +const PaymentConfirmModal = ({ + t, + open, + onlineTopUp, + handleCancel, + confirmLoading, + topUpCount, + renderQuotaWithAmount, + amountLoading, + renderAmount, + payWay, + payMethods, +}) => { + return ( + + + {t('充值确认')} +
+ } + visible={open} + onOk={onlineTopUp} + onCancel={handleCancel} + maskClosable={false} + size='small' + centered + confirmLoading={confirmLoading} + > +
+ +
+
+ {t('充值数量')}: + {renderQuotaWithAmount(topUpCount)} +
+
+ {t('实付金额')}: + {amountLoading ? ( + + ) : ( + + {renderAmount()} + + )} +
+
+ {t('支付方式')}: +
+ {(() => { + const payMethod = payMethods.find( + (method) => method.type === payWay, + ); + if (payMethod) { + return ( + <> + {payMethod.type === 'zfb' ? ( + + ) : payMethod.type === 'wx' ? ( + + ) : ( + + )} + {payMethod.name} + + ); + } else { + // 默认充值方式 + if (payWay === 'zfb') { + return ( + <> + + {t('支付宝')} + + ); + } else if (payWay === 'stripe') { + return ( + <> + + Stripe + + ); + } else { + return ( + <> + + {t('微信')} + + ); + } + } + })()} +
+
+
+
+
+ + ); +}; + +export default PaymentConfirmModal; diff --git a/web/src/components/topup/modals/TransferModal.jsx b/web/src/components/topup/modals/TransferModal.jsx new file mode 100644 index 00000000..c7911378 --- /dev/null +++ b/web/src/components/topup/modals/TransferModal.jsx @@ -0,0 +1,82 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { + Modal, + Typography, + Input, + InputNumber, +} from '@douyinfe/semi-ui'; +import { CreditCard } from 'lucide-react'; + +const TransferModal = ({ + t, + openTransfer, + transfer, + handleTransferCancel, + userState, + renderQuota, + getQuotaPerUnit, + transferAmount, + setTransferAmount, +}) => { + return ( + + + {t('划转邀请额度')} +
+ } + visible={openTransfer} + onOk={transfer} + onCancel={handleTransferCancel} + maskClosable={false} + centered + > +
+
+ + {t('可用邀请额度')} + + +
+
+ + {t('划转额度')} · {t('最低') + renderQuota(getQuotaPerUnit())} + + setTransferAmount(value)} + className='w-full !rounded-lg' + /> +
+
+ + ); +}; + +export default TransferModal; diff --git a/web/src/constants/dashboard.constants.js b/web/src/constants/dashboard.constants.js index 332687e5..cd14a039 100644 --- a/web/src/constants/dashboard.constants.js +++ b/web/src/constants/dashboard.constants.js @@ -21,8 +21,8 @@ For commercial licensing, please contact support@quantumnous.com export const CHART_CONFIG = { mode: 'desktop-browser' }; export const CARD_PROPS = { - shadows: 'always', - bordered: false, + shadows: '', + bordered: true, headerLine: true }; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 1d8f144f..78d0ed35 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -853,8 +853,15 @@ "支付宝": "Alipay", "待使用收益": "Proceeds to be used", "邀请人数": "Number of people invited", + "可随时划转到账户余额": "Can be transferred to the account balance at any time", + "成功邀请": "Successfully invited", + "累计获得": "Accumulated", "兑换码充值": "Redemption code recharge", - "使用兑换码快速充值": "Use redemption code to quickly recharge", + "奖励说明": "Reward description", + "人": "", + "选择支付方式": "Select payment method", + "账户充值": "Account recharge", + "多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient", "支付方式": "Payment method", "邀请奖励": "Invite reward", "或输入自定义金额": "Or enter a custom amount", @@ -937,7 +944,7 @@ "不是合法的 JSON 字符串": "Not a valid JSON string", "个人中心": "Personal center", "代理商": "Agent", - "钱包": "Wallet", + "钱包管理": "Wallet", "备注": "Remark", "工作台": "Workbench", "已复制:": "Copied:", @@ -1218,7 +1225,6 @@ "充值数量": "Recharge quantity", "实付金额": "Actual payment amount", "是否确认充值?": "Confirm recharge?", - "我的钱包": "My wallet", "默认聊天页面链接": "Default chat page link", "聊天页面 2 链接": "Chat page 2 link", "失败重试次数": "Failed retry times", diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index 867e623e..a4d81449 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -17,1314 +17,6 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect, useState, useContext } from 'react'; -import { - API, - showError, - showInfo, - showSuccess, - renderQuota, - renderQuotaWithAmount, - copy, - getQuotaPerUnit, -} from '../../helpers'; -import { - Avatar, - Typography, - Card, - Button, - Modal, - Toast, - Input, - InputNumber, - Banner, - Skeleton, - Divider, -} from '@douyinfe/semi-ui'; -import { SiAlipay, SiWechat } from 'react-icons/si'; -import { useTranslation } from 'react-i18next'; -import { UserContext } from '../../context/User'; -import { StatusContext } from '../../context/Status/index.js'; -import { useTheme } from '../../context/Theme'; -import { - CreditCard, - Gift, - Link as LinkIcon, - Copy, - Users, - User, - Coins, -} from 'lucide-react'; - -const { Text, Title } = Typography; - -const TopUp = () => { - const { t } = useTranslation(); - const [userState, userDispatch] = useContext(UserContext); - const [statusState] = useContext(StatusContext); - const theme = useTheme(); - - const [redemptionCode, setRedemptionCode] = useState(''); - const [topUpCode, setTopUpCode] = 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 [stripeAmount, setStripeAmount] = useState(0.0); - const [stripeMinTopUp, setStripeMinTopUp] = useState(statusState?.status?.stripe_min_topup || 1); - const [stripeTopUpCount, setStripeTopUpCount] = useState(statusState?.status?.stripe_min_topup || 1); - const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false); - const [stripeOpen, setStripeOpen] = useState(false); - - 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 [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 [selectedPreset, setSelectedPreset] = useState(null); - - const getUsername = () => { - if (userState.user) { - return userState.user.username; - } else { - return 'null'; - } - }; - - const getUserRole = () => { - if (!userState.user) return t('普通用户'); - - switch (userState.user.role) { - case 100: - return t('超级管理员'); - case 10: - return t('管理员'); - case 0: - default: - return t('普通用户'); - } - }; - - 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, - }); - setUserQuota((quota) => { - return quota + data; - }); - 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 (!enableOnlineTopUp) { - showError(t('管理员未开启在线充值!')); - return; - } - setPayWay(payment); - setPaymentLoading(true); - try { - await getAmount(); - if (topUpCount < minTopUp) { - showError(t('充值数量不能小于') + minTopUp); - return; - } - setOpen(true); - } catch (error) { - showError(t('获取金额失败')); - } finally { - setPaymentLoading(false); - } - }; - - const onlineTopUp = async () => { - if (amount === 0) { - await getAmount(); - } - if (topUpCount < minTopUp) { - showError('充值数量不能小于' + minTopUp); - return; - } - setConfirmLoading(true); - try { - const res = await API.post('/api/user/pay', { - amount: parseInt(topUpCount), - top_up_code: topUpCode, - payment_method: payWay, - }); - if (res !== undefined) { - const { message, data } = res.data; - if (message === 'success') { - 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 stripePreTopUp = async () => { - if (!enableStripeTopUp) { - showError(t('管理员未开启在线充值!')); - return; - } - setPayWay('stripe'); - setPaymentLoading(true); - try { - await getStripeAmount(); - if (stripeTopUpCount < stripeMinTopUp) { - showError(t('充值数量不能小于') + stripeMinTopUp); - return; - } - setStripeOpen(true); - } catch (error) { - showError(t('获取金额失败')); - } finally { - setPaymentLoading(false); - } - }; - - const onlineStripeTopUp = async () => { - if (stripeAmount === 0) { - await getStripeAmount(); - } - if (stripeTopUpCount < stripeMinTopUp) { - showError(t('充值数量不能小于') + stripeMinTopUp); - return; - } - setConfirmLoading(true); - try { - const res = await API.post('/api/user/stripe/pay', { - amount: parseInt(stripeTopUpCount), - payment_method: 'stripe', - }); - if (res !== undefined) { - const { message, data } = res.data; - if (message === 'success') { - processStripeCallback(data); - } else { - showError(data); - } - } else { - showError(res); - } - } catch (err) { - console.log(err); - showError(t('支付请求失败')); - } finally { - setStripeOpen(false); - setConfirmLoading(false); - } - } - - const processStripeCallback = (data) => { - window.open(data.pay_link, '_blank'); - }; - - 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); - }; - - // 获取邀请链接 - 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(() => { - if (userState?.user?.id) { - setUserDataLoading(false); - setUserQuota(userState.user.quota); - } else { - getUserQuota().then(); - } - getAffLink().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) { - if (method.type === 'zfb') { - method.color = 'rgba(var(--semi-blue-5), 1)'; - } else if (method.type === 'wx') { - method.color = 'rgba(var(--semi-green-5), 1)'; - } else { - method.color = 'rgba(var(--semi-primary-5), 1)'; - } - } - return method; - }); - setPayMethods(payMethods); - } - } catch (e) { - console.log(e); - showError(t('支付方式配置错误, 请联系管理员')); - } - }, []); - - useEffect(() => { - if (statusState?.status) { - setMinTopUp(statusState.status.min_topup || 1); - setTopUpCount(statusState.status.min_topup || 1); - setTopUpLink(statusState.status.top_up_link || ''); - setEnableOnlineTopUp(statusState.status.enable_online_topup || false); - setPriceRatio(statusState.status.price || 1); - - setStripeMinTopUp(statusState.status.stripe_min_topup || 1); - setStripeTopUpCount(statusState.status.stripe_min_topup || 1); - setEnableStripeTopUp(statusState.status.enable_stripe_topup || false); - } - }, [statusState?.status]); - - const renderAmount = () => { - return amount + ' ' + t('元'); - }; - - const renderStripeAmount = () => { - return stripeAmount + ' ' + t('元'); - }; - - const getAmount = async (value) => { - if (value === undefined) { - value = topUpCount; - } - setAmountLoading(true); - try { - const res = await API.post('/api/user/amount', { - amount: parseFloat(value), - top_up_code: topUpCode, - }); - 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 = stripeTopUpCount - } - setAmountLoading(true); - try { - const res = await API.post('/api/user/stripe/amount', { - amount: parseFloat(value), - }); - if (res !== undefined) { - const { message, data } = res.data; - // showInfo(message); - if (message === 'success') { - setStripeAmount(parseFloat(data)); - } else { - setStripeAmount(0); - Toast.error({ content: '错误:' + data, id: 'getAmount' }); - } - } else { - showError(res); - } - } catch (err) { - console.log(err); - } finally { - setAmountLoading(false); - } - } - - const handleCancel = () => { - setOpen(false); - }; - - const handleStripeCancel = () => { - setStripeOpen(false); - }; - - const handleTransferCancel = () => { - setOpenTransfer(false); - }; - - // 选择预设充值额度 - const selectPresetAmount = (preset) => { - setTopUpCount(preset.value); - setSelectedPreset(preset.value); - setAmount(preset.value * priceRatio); - - setStripeTopUpCount(preset.value); - setStripeAmount(preset.value); - }; - - // 格式化大数字显示 - const formatLargeNumber = (num) => { - return num.toString(); - }; - - return ( -
- {/* 划转模态框 */} - - - {t('划转邀请额度')} -
- } - visible={openTransfer} - onOk={transfer} - onCancel={handleTransferCancel} - maskClosable={false} - size='small' - centered - > -
-
- - {t('可用邀请额度')} - - -
-
- - {t('划转额度')} ({t('最低') + renderQuota(getQuotaPerUnit())}) - - setTransferAmount(value)} - size='large' - className='w-full' - /> -
-
- - - {/* 充值确认模态框 */} - - - {t('充值确认')} -
- } - visible={open} - onOk={onlineTopUp} - onCancel={handleCancel} - maskClosable={false} - size='small' - centered - confirmLoading={confirmLoading} - > -
-
- {t('充值数量')}: - {renderQuotaWithAmount(topUpCount)} -
-
- {t('实付金额')}: - {amountLoading ? ( - - ) : ( - - {renderAmount()} - - )} -
-
- {t('支付方式')}: - - {(() => { - const payMethod = payMethods.find( - (method) => method.type === payWay, - ); - if (payMethod) { - return ( -
- {payMethod.type === 'zfb' ? ( - - ) : payMethod.type === 'wx' ? ( - - ) : ( - - )} - {payMethod.name} -
- ); - } else { - // 默认充值方式 - return payWay === 'zfb' ? ( -
- - {t('支付宝')} -
- ) : ( -
- - {t('微信')} -
- ); - } - })()} -
-
-
- - - -

- {t('充值数量')}:{stripeTopUpCount} -

-

- {t('实付金额')}:{renderStripeAmount()} -

-

{t('是否确认充值?')}

-
- -
- {/* 左侧充值区域 */} -
- {/* 在线充值卡片 */} - -
-
- - - -
- - {t('在线充值')} - - - {t('快速方便的充值方式')} - -
-
- -
- {userDataLoading ? ( - - ) : ( - -
- - - {getUsername()} ({getUserRole()}) - - {getUsername()} -
-
- )} -
-
-
- } - > -
- {/* 账户余额信息 */} -
- - - {t('当前余额')} - - {userDataLoading ? ( - - ) : ( -
- {renderQuota(userState?.user?.quota || userQuota)} -
- )} -
- - - {t('历史消耗')} - - {userDataLoading ? ( - - ) : ( -
- {renderQuota(userState?.user?.used_quota || 0)} -
- )} -
-
- - {enableOnlineTopUp && ( - <> - {/* 预设充值额度卡片网格 */} -
- - {t('选择充值额度')} - -
- {presetAmounts.map((preset, index) => ( - selectPresetAmount(preset)} - className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${selectedPreset === preset.value - ? 'border-blue-500' - : 'border-gray-200 hover:border-gray-300' - }`} - bodyStyle={{ textAlign: 'center' }} - > -
- - {formatLargeNumber(preset.value)} -
-
- {t('实付')} ¥ - {(preset.value * priceRatio).toFixed(2)} -
-
- ))} -
-
- {/* 桌面端显示的自定义金额和支付按钮 */} -
- - - {t('或输入自定义金额')} - - - -
-
- {t('充值数量')} - {amountLoading ? ( - - ) : ( - - {t('实付金额:') + renderAmount()} - - )} -
- { - if (value && value >= 1) { - setTopUpCount(value); - setSelectedPreset(null); - await getAmount(value); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (!value || value < 1) { - setTopUpCount(1); - getAmount(1); - } - }} - size='large' - className='w-full' - formatter={(value) => (value ? `${value}` : '')} - parser={(value) => - value ? parseInt(value.replace(/[^\d]/g, '')) : 0 - } - /> -
- -
- - {t('选择支付方式')} - - {payMethods.length === 2 ? ( -
- {payMethods.map((payMethod) => ( - - ))} -
- ) : payMethods.length === 3 ? ( -
- {payMethods.map((payMethod) => ( - - ))} -
- ) : payMethods.length > 3 ? ( -
- {payMethods.map((payMethod) => ( - preTopUp(payMethod.type)} - disabled={!enableOnlineTopUp} - className={`cursor-pointer !rounded-xl p-0 transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type - ? 'border-blue-400' - : 'border-gray-200 hover:border-gray-300' - }`} - bodyStyle={{ - padding: '10px', - textAlign: 'center', - opacity: !enableOnlineTopUp ? 0.5 : 1 - }} - > - {paymentLoading && payWay === payMethod.type ? ( -
-
-
-
-
{t('处理中')}
-
- ) : ( - <> -
- {payMethod.type === 'zfb' ? ( - - ) : payMethod.type === 'wx' ? ( - - ) : ( - - )} -
-
{payMethod.name}
- - )} -
- ))} -
- ) : ( -
- {payMethods.map((payMethod) => ( - - ))} -
- )} -
-
- - )} - - {!enableOnlineTopUp && !enableStripeTopUp && ( - - )} - - {enableStripeTopUp && ( - <> - {/* 桌面端显示的自定义金额和支付按钮 */} -
- - - {t(!enableOnlineTopUp ? '或输入自定义金额' : 'Stripe')} - - - -
-
- {t('充值数量')} - {amountLoading ? ( - - ) : ( - - {t('实付金额:') + renderStripeAmount()} - - )} -
- { - if (value && value >= 1) { - setStripeTopUpCount(value); - setSelectedPreset(null); - await getStripeAmount(value); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (!value || value < 1) { - setStripeTopUpCount(1); - getStripeAmount(1); - } - }} - size='large' - className='w-full' - formatter={(value) => (value ? `${value}` : '')} - parser={(value) => - value ? parseInt(value.replace(/[^\d]/g, '')) : 0 - } - /> -
- -
- - {t('选择支付方式')} - -
- -
-
-
- - )} - - - {t('兑换码充值')} - - - -
- - {t('使用兑换码快速充值')} -
- -
- setRedemptionCode(value)} - size='large' - /> -
- -
- {topUpLink && ( - - )} - -
-
-
- -
- - {/* 右侧邀请信息卡片 */} -
- -
-
- - - -
- - {t('邀请奖励')} - - - {t('邀请好友获得额外奖励')} - -
-
-
-
- } - > -
-
- -
- {t('待使用收益')} - -
-
- {renderQuota(userState?.user?.aff_quota || 0)} -
-
- -
- - {t('总收益')} -
- {renderQuota(userState?.user?.aff_history_quota || 0)} -
-
- - {t('邀请人数')} -
- - {userState?.user?.aff_count || 0} -
-
-
-
- -
- {t('邀请链接')} - } - > - {t('复制')} - - } - /> - -
- -
-
-
- - {t('邀请好友注册,好友充值后您可获得相应奖励')} - -
-
-
- - {t('通过划转功能将奖励额度转入到您的账户余额中')} - -
-
-
- - {t('邀请的好友越多,获得的奖励越多')} - -
-
-
-
-
-
- -
-
- - {/* 移动端底部固定的自定义金额和支付区域 */} - {enableOnlineTopUp && ( -
-
-
-
- {t('充值数量')} - {amountLoading ? ( - - ) : ( - - {t('实付金额:') + renderAmount()} - - )} -
- { - if (value && value >= 1) { - setTopUpCount(value); - setSelectedPreset(null); - await getAmount(value); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (!value || value < 1) { - setTopUpCount(1); - getAmount(1); - } - }} - className='w-full' - formatter={(value) => (value ? `${value}` : '')} - parser={(value) => - value ? parseInt(value.replace(/[^\d]/g, '')) : 0 - } - /> -
- -
- {payMethods.length === 2 ? ( -
- {payMethods.map((payMethod) => ( - - ))} -
- ) : ( -
- {payMethods.map((payMethod) => ( - preTopUp(payMethod.type)} - disabled={!enableOnlineTopUp} - className={`cursor-pointer !rounded-xl p-0 transition-all ${paymentLoading && payWay === payMethod.type - ? 'border-blue-400' - : 'border-gray-200' - }`} - bodyStyle={{ - padding: '8px', - textAlign: 'center', - opacity: !enableOnlineTopUp ? 0.5 : 1 - }} - > - {paymentLoading && payWay === payMethod.type ? ( -
- ) : ( - <> -
- {payMethod.type === 'zfb' ? ( - - ) : payMethod.type === 'wx' ? ( - - ) : ( - - )} -
-
{payMethod.name}
- - )} -
- ))} -
- )} -
-
-
- )} -
- ); -}; +import TopUp from '../../components/topup'; export default TopUp;