Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 

847 righe
26 KiB

  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState, useContext, useRef } from 'react';
  16. import {
  17. API,
  18. showError,
  19. showInfo,
  20. showSuccess,
  21. renderQuota,
  22. renderQuotaWithAmount,
  23. copy,
  24. getQuotaPerUnit,
  25. } from '../../helpers';
  26. import { Modal, Toast } from '@douyinfe/semi-ui';
  27. import { useTranslation } from 'react-i18next';
  28. import { UserContext } from '../../context/User';
  29. import { StatusContext } from '../../context/Status';
  30. import RechargeCard from './RechargeCard';
  31. import InvitationCard from './InvitationCard';
  32. import TransferModal from './modals/TransferModal';
  33. import PaymentConfirmModal from './modals/PaymentConfirmModal';
  34. import TopupHistoryModal from './modals/TopupHistoryModal';
  35. import WechatPayQRCodeModal from './WechatPayQRCodeModal';
  36. const TopUp = () => {
  37. const { t } = useTranslation();
  38. const [userState, userDispatch] = useContext(UserContext);
  39. const [statusState] = useContext(StatusContext);
  40. const [redemptionCode, setRedemptionCode] = useState('');
  41. const [amount, setAmount] = useState(0.0);
  42. const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1);
  43. const [topUpCount, setTopUpCount] = useState(
  44. statusState?.status?.min_topup || 1,
  45. );
  46. const [topUpLink, setTopUpLink] = useState(
  47. statusState?.status?.top_up_link || '',
  48. );
  49. const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(
  50. statusState?.status?.enable_online_topup || false,
  51. );
  52. const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1);
  53. const [enableStripeTopUp, setEnableStripeTopUp] = useState(
  54. statusState?.status?.enable_stripe_topup || false,
  55. );
  56. const [statusLoading, setStatusLoading] = useState(true);
  57. // Creem 相关状态
  58. const [creemProducts, setCreemProducts] = useState([]);
  59. const [enableCreemTopUp, setEnableCreemTopUp] = useState(false);
  60. const [creemOpen, setCreemOpen] = useState(false);
  61. const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
  62. const [isSubmitting, setIsSubmitting] = useState(false);
  63. const [open, setOpen] = useState(false);
  64. const [payWay, setPayWay] = useState('');
  65. const [amountLoading, setAmountLoading] = useState(false);
  66. const [paymentLoading, setPaymentLoading] = useState(false);
  67. const [confirmLoading, setConfirmLoading] = useState(false);
  68. const [payMethods, setPayMethods] = useState([]);
  69. const affFetchedRef = useRef(false);
  70. // 邀请相关状态
  71. const [affLink, setAffLink] = useState('');
  72. const [openTransfer, setOpenTransfer] = useState(false);
  73. const [transferAmount, setTransferAmount] = useState(0);
  74. // 账单Modal状态
  75. const [openHistory, setOpenHistory] = useState(false);
  76. // 微信支付相关状态
  77. const [wechatPayVisible, setWechatPayVisible] = useState(false);
  78. const [wechatPayQRCodeUrl, setWechatPayQRCodeUrl] = useState('');
  79. const [wechatPayTradeNo, setWechatPayTradeNo] = useState('');
  80. const [enableWechatTopUp, setEnableWechatTopUp] = useState(false);
  81. // 订阅相关
  82. const [subscriptionPlans, setSubscriptionPlans] = useState([]);
  83. const [subscriptionLoading, setSubscriptionLoading] = useState(true);
  84. const [billingPreference, setBillingPreference] =
  85. useState('subscription_first');
  86. const [activeSubscriptions, setActiveSubscriptions] = useState([]);
  87. const [allSubscriptions, setAllSubscriptions] = useState([]);
  88. // 预设充值额度选项
  89. const [presetAmounts, setPresetAmounts] = useState([]);
  90. const [selectedPreset, setSelectedPreset] = useState(null);
  91. // 充值配置信息
  92. const [topupInfo, setTopupInfo] = useState({
  93. amount_options: [],
  94. discount: {},
  95. });
  96. const topUp = async () => {
  97. if (redemptionCode === '') {
  98. showInfo(t('请输入兑换码!'));
  99. return;
  100. }
  101. setIsSubmitting(true);
  102. try {
  103. const res = await API.post('/api/user/topup', {
  104. key: redemptionCode,
  105. });
  106. const { success, message, data } = res.data;
  107. if (success) {
  108. showSuccess(t('兑换成功!'));
  109. Modal.success({
  110. title: t('兑换成功!'),
  111. content: t('成功兑换额度:') + renderQuota(data),
  112. centered: true,
  113. });
  114. if (userState.user) {
  115. const updatedUser = {
  116. ...userState.user,
  117. quota: userState.user.quota + data,
  118. };
  119. userDispatch({ type: 'login', payload: updatedUser });
  120. }
  121. setRedemptionCode('');
  122. } else {
  123. showError(message);
  124. }
  125. } catch (err) {
  126. showError(t('请求失败'));
  127. } finally {
  128. setIsSubmitting(false);
  129. }
  130. };
  131. const openTopUpLink = () => {
  132. if (!topUpLink) {
  133. showError(t('超级管理员未设置充值链接!'));
  134. return;
  135. }
  136. window.open(topUpLink, '_blank');
  137. };
  138. const preTopUp = async (payment) => {
  139. if (payment === 'stripe') {
  140. if (!enableStripeTopUp) {
  141. showError(t('管理员未开启Stripe充值!'));
  142. return;
  143. }
  144. } else if (payment === 'wechat_pay') {
  145. if (!enableWechatTopUp) {
  146. showError(t('管理员未开启微信支付充值!'));
  147. return;
  148. }
  149. } else {
  150. if (!enableOnlineTopUp) {
  151. showError(t('管理员未开启在线充值!'));
  152. return;
  153. }
  154. }
  155. setPayWay(payment);
  156. // 微信支付直接创建订单并显示二维码
  157. if (payment === 'wechat_pay') {
  158. setPaymentLoading(true);
  159. try {
  160. const res = await API.post('/api/user/wechat/pay', {
  161. amount: parseInt(topUpCount),
  162. });
  163. const { message, data } = res.data;
  164. if (message === 'success') {
  165. setWechatPayQRCodeUrl(data.qr_code_url);
  166. setWechatPayTradeNo(data.trade_no);
  167. setWechatPayVisible(true);
  168. } else {
  169. const errorMsg = typeof data === 'string' ? data : message || t('支付失败');
  170. showError(errorMsg);
  171. }
  172. } catch (err) {
  173. showError(t('支付请求失败'));
  174. } finally {
  175. setPaymentLoading(false);
  176. }
  177. return;
  178. }
  179. setPaymentLoading(true);
  180. try {
  181. if (payment === 'stripe') {
  182. await getStripeAmount();
  183. } else {
  184. await getAmount();
  185. }
  186. if (topUpCount < minTopUp) {
  187. showError(t('充值数量不能小于') + minTopUp);
  188. return;
  189. }
  190. setOpen(true);
  191. } catch (error) {
  192. showError(t('获取金额失败'));
  193. } finally {
  194. setPaymentLoading(false);
  195. }
  196. };
  197. const onlineTopUp = async () => {
  198. if (payWay === 'stripe') {
  199. // Stripe 支付处理
  200. if (amount === 0) {
  201. await getStripeAmount();
  202. }
  203. } else {
  204. // 普通支付处理
  205. if (amount === 0) {
  206. await getAmount();
  207. }
  208. }
  209. if (topUpCount < minTopUp) {
  210. showError('充值数量不能小于' + minTopUp);
  211. return;
  212. }
  213. setConfirmLoading(true);
  214. try {
  215. let res;
  216. if (payWay === 'stripe') {
  217. // Stripe 支付请求
  218. res = await API.post('/api/user/stripe/pay', {
  219. amount: parseInt(topUpCount),
  220. payment_method: 'stripe',
  221. });
  222. } else {
  223. // 普通支付请求
  224. res = await API.post('/api/user/pay', {
  225. amount: parseInt(topUpCount),
  226. payment_method: payWay,
  227. });
  228. }
  229. if (res !== undefined) {
  230. const { message, data } = res.data;
  231. if (message === 'success') {
  232. if (payWay === 'stripe') {
  233. // Stripe 支付回调处理
  234. window.open(data.pay_link, '_blank');
  235. } else {
  236. // 普通支付表单提交
  237. let params = data;
  238. let url = res.data.url;
  239. let form = document.createElement('form');
  240. form.action = url;
  241. form.method = 'POST';
  242. let isSafari =
  243. navigator.userAgent.indexOf('Safari') > -1 &&
  244. navigator.userAgent.indexOf('Chrome') < 1;
  245. if (!isSafari) {
  246. form.target = '_blank';
  247. }
  248. for (let key in params) {
  249. let input = document.createElement('input');
  250. input.type = 'hidden';
  251. input.name = key;
  252. input.value = params[key];
  253. form.appendChild(input);
  254. }
  255. document.body.appendChild(form);
  256. form.submit();
  257. document.body.removeChild(form);
  258. }
  259. } else {
  260. const errorMsg =
  261. typeof data === 'string' ? data : message || t('支付失败');
  262. showError(errorMsg);
  263. }
  264. } else {
  265. showError(res);
  266. }
  267. } catch (err) {
  268. console.log(err);
  269. showError(t('支付请求失败'));
  270. } finally {
  271. setOpen(false);
  272. setConfirmLoading(false);
  273. }
  274. };
  275. const creemPreTopUp = async (product) => {
  276. if (!enableCreemTopUp) {
  277. showError(t('管理员未开启 Creem 充值!'));
  278. return;
  279. }
  280. setSelectedCreemProduct(product);
  281. setCreemOpen(true);
  282. };
  283. const onlineCreemTopUp = async () => {
  284. if (!selectedCreemProduct) {
  285. showError(t('请选择产品'));
  286. return;
  287. }
  288. // Validate product has required fields
  289. if (!selectedCreemProduct.productId) {
  290. showError(t('产品配置错误,请联系管理员'));
  291. return;
  292. }
  293. setConfirmLoading(true);
  294. try {
  295. const res = await API.post('/api/user/creem/pay', {
  296. product_id: selectedCreemProduct.productId,
  297. payment_method: 'creem',
  298. });
  299. if (res !== undefined) {
  300. const { message, data } = res.data;
  301. if (message === 'success') {
  302. processCreemCallback(data);
  303. } else {
  304. const errorMsg =
  305. typeof data === 'string' ? data : message || t('支付失败');
  306. showError(errorMsg);
  307. }
  308. } else {
  309. showError(res);
  310. }
  311. } catch (err) {
  312. console.log(err);
  313. showError(t('支付请求失败'));
  314. } finally {
  315. setCreemOpen(false);
  316. setConfirmLoading(false);
  317. }
  318. };
  319. const processCreemCallback = (data) => {
  320. // 与 Stripe 保持一致的实现方式
  321. window.open(data.checkout_url, '_blank');
  322. };
  323. const getUserQuota = async () => {
  324. let res = await API.get(`/api/user/self`);
  325. const { success, message, data } = res.data;
  326. if (success) {
  327. userDispatch({ type: 'login', payload: data });
  328. } else {
  329. showError(message);
  330. }
  331. };
  332. const getSubscriptionPlans = async () => {
  333. setSubscriptionLoading(true);
  334. try {
  335. const res = await API.get('/api/subscription/plans');
  336. if (res.data?.success) {
  337. setSubscriptionPlans(res.data.data || []);
  338. }
  339. } catch (e) {
  340. setSubscriptionPlans([]);
  341. } finally {
  342. setSubscriptionLoading(false);
  343. }
  344. };
  345. const getSubscriptionSelf = async () => {
  346. try {
  347. const res = await API.get('/api/subscription/self');
  348. if (res.data?.success) {
  349. setBillingPreference(
  350. res.data.data?.billing_preference || 'subscription_first',
  351. );
  352. // Active subscriptions
  353. const activeSubs = res.data.data?.subscriptions || [];
  354. setActiveSubscriptions(activeSubs);
  355. // All subscriptions (including expired)
  356. const allSubs = res.data.data?.all_subscriptions || [];
  357. setAllSubscriptions(allSubs);
  358. }
  359. } catch (e) {
  360. // ignore
  361. }
  362. };
  363. const updateBillingPreference = async (pref) => {
  364. const previousPref = billingPreference;
  365. setBillingPreference(pref);
  366. try {
  367. const res = await API.put('/api/subscription/self/preference', {
  368. billing_preference: pref,
  369. });
  370. if (res.data?.success) {
  371. showSuccess(t('更新成功'));
  372. const normalizedPref =
  373. res.data?.data?.billing_preference || pref || previousPref;
  374. setBillingPreference(normalizedPref);
  375. } else {
  376. showError(res.data?.message || t('更新失败'));
  377. setBillingPreference(previousPref);
  378. }
  379. } catch (e) {
  380. showError(t('请求失败'));
  381. setBillingPreference(previousPref);
  382. }
  383. };
  384. // 获取充值配置信息
  385. const getTopupInfo = async () => {
  386. try {
  387. const res = await API.get('/api/user/topup/info');
  388. const { message, data, success } = res.data;
  389. if (success) {
  390. setTopupInfo({
  391. amount_options: data.amount_options || [],
  392. discount: data.discount || {},
  393. });
  394. // 处理支付方式
  395. let payMethods = data.pay_methods || [];
  396. try {
  397. if (typeof payMethods === 'string') {
  398. payMethods = JSON.parse(payMethods);
  399. }
  400. if (payMethods && payMethods.length > 0) {
  401. // 检查name和type是否为空
  402. payMethods = payMethods.filter((method) => {
  403. return method.name && method.type;
  404. });
  405. // 如果没有color,则设置默认颜色
  406. payMethods = payMethods.map((method) => {
  407. // 规范化最小充值数
  408. const normalizedMinTopup = Number(method.min_topup);
  409. method.min_topup = Number.isFinite(normalizedMinTopup)
  410. ? normalizedMinTopup
  411. : 0;
  412. // Stripe 的最小充值从后端字段回填
  413. if (
  414. method.type === 'stripe' &&
  415. (!method.min_topup || method.min_topup <= 0)
  416. ) {
  417. const stripeMin = Number(data.stripe_min_topup);
  418. if (Number.isFinite(stripeMin)) {
  419. method.min_topup = stripeMin;
  420. }
  421. }
  422. if (!method.color) {
  423. if (method.type === 'alipay') {
  424. method.color = 'rgba(var(--semi-blue-5), 1)';
  425. } else if (method.type === 'wxpay') {
  426. method.color = 'rgba(var(--semi-green-5), 1)';
  427. } else if (method.type === 'stripe') {
  428. method.color = 'rgba(var(--semi-purple-5), 1)';
  429. } else {
  430. method.color = 'rgba(var(--semi-primary-5), 1)';
  431. }
  432. }
  433. return method;
  434. });
  435. } else {
  436. payMethods = [];
  437. }
  438. // 如果启用了 Stripe 支付,添加到支付方法列表
  439. // 这个逻辑现在由后端处理,如果 Stripe 启用,后端会在 pay_methods 中包含它
  440. setPayMethods(payMethods);
  441. const enableStripeTopUp = data.enable_stripe_topup || false;
  442. const enableOnlineTopUp = data.enable_online_topup || false;
  443. const enableCreemTopUp = data.enable_creem_topup || false;
  444. const enableWechatTopUpVal = data.enable_wechat_topup || false;
  445. const minTopUpValue = enableOnlineTopUp
  446. ? data.min_topup
  447. : enableStripeTopUp
  448. ? data.stripe_min_topup
  449. : enableWechatTopUpVal
  450. ? data.wechat_pay_min_topup || 1
  451. : 1;
  452. setEnableOnlineTopUp(enableOnlineTopUp);
  453. setEnableStripeTopUp(enableStripeTopUp);
  454. setEnableCreemTopUp(enableCreemTopUp);
  455. setEnableWechatTopUp(enableWechatTopUpVal);
  456. setMinTopUp(minTopUpValue);
  457. setTopUpCount(minTopUpValue);
  458. // 设置 Creem 产品
  459. try {
  460. console.log(' data is ?', data);
  461. console.log(' creem products is ?', data.creem_products);
  462. const products = JSON.parse(data.creem_products || '[]');
  463. setCreemProducts(products);
  464. } catch (e) {
  465. setCreemProducts([]);
  466. }
  467. // 如果没有自定义充值数量选项,根据最小充值金额生成预设充值额度选项
  468. if (topupInfo.amount_options.length === 0) {
  469. setPresetAmounts(generatePresetAmounts(minTopUpValue));
  470. }
  471. // 初始化显示实付金额
  472. getAmount(minTopUpValue);
  473. } catch (e) {
  474. console.log('解析支付方式失败:', e);
  475. setPayMethods([]);
  476. }
  477. // 如果有自定义充值数量选项,使用它们替换默认的预设选项
  478. if (data.amount_options && data.amount_options.length > 0) {
  479. const customPresets = data.amount_options.map((amount) => ({
  480. value: amount,
  481. discount: data.discount[amount] || 1.0,
  482. }));
  483. setPresetAmounts(customPresets);
  484. }
  485. } else {
  486. console.error('获取充值配置失败:', data);
  487. }
  488. } catch (error) {
  489. console.error('获取充值配置异常:', error);
  490. }
  491. };
  492. // 获取邀请链接
  493. const getAffLink = async () => {
  494. const res = await API.get('/api/user/aff');
  495. const { success, message, data } = res.data;
  496. if (success) {
  497. let link = `${window.location.origin}/register?aff=${data}`;
  498. setAffLink(link);
  499. } else {
  500. showError(message);
  501. }
  502. };
  503. // 划转邀请额度
  504. const transfer = async () => {
  505. if (transferAmount < getQuotaPerUnit()) {
  506. showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
  507. return;
  508. }
  509. const res = await API.post(`/api/user/aff_transfer`, {
  510. quota: transferAmount,
  511. });
  512. const { success, message } = res.data;
  513. if (success) {
  514. showSuccess(message);
  515. setOpenTransfer(false);
  516. getUserQuota().then();
  517. } else {
  518. showError(message);
  519. }
  520. };
  521. // 复制邀请链接
  522. const handleAffLinkClick = async () => {
  523. await copy(affLink);
  524. showSuccess(t('邀请链接已复制到剪切板'));
  525. };
  526. useEffect(() => {
  527. // 始终获取最新用户数据,确保余额等统计信息准确
  528. getUserQuota().then();
  529. setTransferAmount(getQuotaPerUnit());
  530. }, []);
  531. useEffect(() => {
  532. if (affFetchedRef.current) return;
  533. affFetchedRef.current = true;
  534. getAffLink().then();
  535. }, []);
  536. // 在 statusState 可用时获取充值信息
  537. useEffect(() => {
  538. getTopupInfo().then();
  539. getSubscriptionPlans().then();
  540. getSubscriptionSelf().then();
  541. }, []);
  542. useEffect(() => {
  543. if (statusState?.status) {
  544. // const minTopUpValue = statusState.status.min_topup || 1;
  545. // setMinTopUp(minTopUpValue);
  546. // setTopUpCount(minTopUpValue);
  547. setTopUpLink(statusState.status.top_up_link || '');
  548. setPriceRatio(statusState.status.price || 1);
  549. setStatusLoading(false);
  550. }
  551. }, [statusState?.status]);
  552. const renderAmount = () => {
  553. return amount + ' ' + t('元');
  554. };
  555. const getAmount = async (value) => {
  556. if (value === undefined) {
  557. value = topUpCount;
  558. }
  559. setAmountLoading(true);
  560. try {
  561. const res = await API.post('/api/user/amount', {
  562. amount: parseFloat(value),
  563. });
  564. if (res !== undefined) {
  565. const { message, data } = res.data;
  566. if (message === 'success') {
  567. setAmount(parseFloat(data));
  568. } else {
  569. setAmount(0);
  570. Toast.error({ content: '错误:' + data, id: 'getAmount' });
  571. }
  572. } else {
  573. showError(res);
  574. }
  575. } catch (err) {
  576. console.log(err);
  577. }
  578. setAmountLoading(false);
  579. };
  580. const getStripeAmount = async (value) => {
  581. if (value === undefined) {
  582. value = topUpCount;
  583. }
  584. setAmountLoading(true);
  585. try {
  586. const res = await API.post('/api/user/stripe/amount', {
  587. amount: parseFloat(value),
  588. });
  589. if (res !== undefined) {
  590. const { message, data } = res.data;
  591. if (message === 'success') {
  592. setAmount(parseFloat(data));
  593. } else {
  594. setAmount(0);
  595. Toast.error({ content: '错误:' + data, id: 'getAmount' });
  596. }
  597. } else {
  598. showError(res);
  599. }
  600. } catch (err) {
  601. console.log(err);
  602. } finally {
  603. setAmountLoading(false);
  604. }
  605. };
  606. const handleCancel = () => {
  607. setOpen(false);
  608. };
  609. const handleTransferCancel = () => {
  610. setOpenTransfer(false);
  611. };
  612. const handleOpenHistory = () => {
  613. setOpenHistory(true);
  614. };
  615. const handleHistoryCancel = () => {
  616. setOpenHistory(false);
  617. };
  618. const handleCreemCancel = () => {
  619. setCreemOpen(false);
  620. setSelectedCreemProduct(null);
  621. };
  622. // 选择预设充值额度
  623. const selectPresetAmount = (preset) => {
  624. setTopUpCount(preset.value);
  625. setSelectedPreset(preset.value);
  626. // 计算实际支付金额,考虑折扣
  627. const discount = preset.discount || topupInfo.discount[preset.value] || 1.0;
  628. const discountedAmount = preset.value * priceRatio * discount;
  629. setAmount(discountedAmount);
  630. };
  631. // 格式化大数字显示
  632. const formatLargeNumber = (num) => {
  633. return num.toString();
  634. };
  635. // 根据最小充值金额生成预设充值额度选项
  636. const generatePresetAmounts = (minAmount) => {
  637. const multipliers = [1, 5, 10, 30, 50, 100, 300, 500];
  638. return multipliers.map((multiplier) => ({
  639. value: minAmount * multiplier,
  640. }));
  641. };
  642. return (
  643. <div className='w-full max-w-7xl mx-auto relative min-h-screen lg:min-h-0 mt-[60px] px-2'>
  644. {/* 划转模态框 */}
  645. <TransferModal
  646. t={t}
  647. openTransfer={openTransfer}
  648. transfer={transfer}
  649. handleTransferCancel={handleTransferCancel}
  650. userState={userState}
  651. renderQuota={renderQuota}
  652. getQuotaPerUnit={getQuotaPerUnit}
  653. transferAmount={transferAmount}
  654. setTransferAmount={setTransferAmount}
  655. />
  656. {/* 充值确认模态框 */}
  657. <PaymentConfirmModal
  658. t={t}
  659. open={open}
  660. onlineTopUp={onlineTopUp}
  661. handleCancel={handleCancel}
  662. confirmLoading={confirmLoading}
  663. topUpCount={topUpCount}
  664. renderQuotaWithAmount={renderQuotaWithAmount}
  665. amountLoading={amountLoading}
  666. renderAmount={renderAmount}
  667. payWay={payWay}
  668. payMethods={payMethods}
  669. amountNumber={amount}
  670. discountRate={topupInfo?.discount?.[topUpCount] || 1.0}
  671. />
  672. {/* 充值账单模态框 */}
  673. <TopupHistoryModal
  674. visible={openHistory}
  675. onCancel={handleHistoryCancel}
  676. t={t}
  677. />
  678. {/* 微信支付二维码弹窗 */}
  679. <WechatPayQRCodeModal
  680. visible={wechatPayVisible}
  681. qrCodeUrl={wechatPayQRCodeUrl}
  682. tradeNo={wechatPayTradeNo}
  683. onClose={() => setWechatPayVisible(false)}
  684. onSuccess={() => {
  685. setWechatPayVisible(false);
  686. showSuccess(t('充值成功!'));
  687. userDispatch({ type: 'refresh' });
  688. }}
  689. />
  690. {/* Creem 充值确认模态框 */}
  691. <Modal
  692. title={t('确定要充值 $')}
  693. visible={creemOpen}
  694. onOk={onlineCreemTopUp}
  695. onCancel={handleCreemCancel}
  696. maskClosable={false}
  697. size='small'
  698. centered
  699. confirmLoading={confirmLoading}
  700. >
  701. {selectedCreemProduct && (
  702. <>
  703. <p>
  704. {t('产品名称')}:{selectedCreemProduct.name}
  705. </p>
  706. <p>
  707. {t('价格')}:{selectedCreemProduct.currency === 'EUR' ? '€' : '$'}
  708. {selectedCreemProduct.price}
  709. </p>
  710. <p>
  711. {t('充值额度')}:{selectedCreemProduct.quota}
  712. </p>
  713. <p>{t('是否确认充值?')}</p>
  714. </>
  715. )}
  716. </Modal>
  717. {/* 主布局区域 */}
  718. <div className='grid grid-cols-1 lg:grid-cols-2 gap-6'>
  719. <RechargeCard
  720. t={t}
  721. enableOnlineTopUp={enableOnlineTopUp}
  722. enableStripeTopUp={enableStripeTopUp}
  723. enableCreemTopUp={enableCreemTopUp}
  724. creemProducts={creemProducts}
  725. creemPreTopUp={creemPreTopUp}
  726. presetAmounts={presetAmounts}
  727. selectedPreset={selectedPreset}
  728. selectPresetAmount={selectPresetAmount}
  729. formatLargeNumber={formatLargeNumber}
  730. priceRatio={priceRatio}
  731. topUpCount={topUpCount}
  732. minTopUp={minTopUp}
  733. renderQuotaWithAmount={renderQuotaWithAmount}
  734. getAmount={getAmount}
  735. setTopUpCount={setTopUpCount}
  736. setSelectedPreset={setSelectedPreset}
  737. renderAmount={renderAmount}
  738. amountLoading={amountLoading}
  739. payMethods={payMethods}
  740. preTopUp={preTopUp}
  741. paymentLoading={paymentLoading}
  742. payWay={payWay}
  743. redemptionCode={redemptionCode}
  744. setRedemptionCode={setRedemptionCode}
  745. topUp={topUp}
  746. isSubmitting={isSubmitting}
  747. topUpLink={topUpLink}
  748. openTopUpLink={openTopUpLink}
  749. userState={userState}
  750. renderQuota={renderQuota}
  751. statusLoading={statusLoading}
  752. topupInfo={topupInfo}
  753. onOpenHistory={handleOpenHistory}
  754. subscriptionLoading={subscriptionLoading}
  755. subscriptionPlans={subscriptionPlans}
  756. billingPreference={billingPreference}
  757. onChangeBillingPreference={updateBillingPreference}
  758. activeSubscriptions={activeSubscriptions}
  759. allSubscriptions={allSubscriptions}
  760. reloadSubscriptionSelf={getSubscriptionSelf}
  761. />
  762. <InvitationCard
  763. t={t}
  764. userState={userState}
  765. renderQuota={renderQuota}
  766. setOpenTransfer={setOpenTransfer}
  767. affLink={affLink}
  768. handleAffLinkClick={handleAffLinkClick}
  769. />
  770. </div>
  771. </div>
  772. );
  773. };
  774. export default TopUp;