index.js 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550
  1. import React, { useEffect, useState, useContext } from 'react';
  2. import {
  3. API,
  4. showError,
  5. showInfo,
  6. showSuccess,
  7. renderQuota,
  8. renderQuotaWithAmount,
  9. copy,
  10. getQuotaPerUnit,
  11. } from '../../helpers';
  12. import {
  13. Avatar,
  14. Typography,
  15. Card,
  16. Button,
  17. Modal,
  18. Toast,
  19. Input,
  20. InputNumber,
  21. Banner,
  22. Skeleton,
  23. Divider,
  24. } from '@douyinfe/semi-ui';
  25. import { SiAlipay, SiWechat } from 'react-icons/si';
  26. import { useTranslation } from 'react-i18next';
  27. import { UserContext } from '../../context/User';
  28. import { StatusContext } from '../../context/Status/index.js';
  29. import { useTheme } from '../../context/Theme';
  30. import {
  31. CreditCard,
  32. Gift,
  33. Link as LinkIcon,
  34. Copy,
  35. Users,
  36. User,
  37. Coins,
  38. } from 'lucide-react';
  39. const { Text, Title } = Typography;
  40. const TopUp = () => {
  41. const { t } = useTranslation();
  42. const [userState, userDispatch] = useContext(UserContext);
  43. const [statusState] = useContext(StatusContext);
  44. const theme = useTheme();
  45. const [redemptionCode, setRedemptionCode] = useState('');
  46. const [topUpCode, setTopUpCode] = useState('');
  47. const [amount, setAmount] = useState(0.0);
  48. const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1);
  49. const [topUpCount, setTopUpCount] = useState(
  50. statusState?.status?.min_topup || 1,
  51. );
  52. const [topUpLink, setTopUpLink] = useState(
  53. statusState?.status?.top_up_link || '',
  54. );
  55. const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(
  56. statusState?.status?.enable_online_topup || false,
  57. );
  58. const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1);
  59. const [stripeAmount, setStripeAmount] = useState(0.0);
  60. const [stripeMinTopUp, setStripeMinTopUp] = useState(statusState?.status?.stripe_min_topup || 1);
  61. const [stripeTopUpCount, setStripeTopUpCount] = useState(statusState?.status?.stripe_min_topup || 1);
  62. const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
  63. const [stripeOpen, setStripeOpen] = useState(false);
  64. const [creemProducts, setCreemProducts] = useState([]);
  65. const [enableCreemTopUp, setEnableCreemTopUp] = useState(false);
  66. const [creemOpen, setCreemOpen] = useState(false);
  67. const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
  68. const [userQuota, setUserQuota] = useState(0);
  69. const [isSubmitting, setIsSubmitting] = useState(false);
  70. const [open, setOpen] = useState(false);
  71. const [payWay, setPayWay] = useState('');
  72. const [userDataLoading, setUserDataLoading] = useState(true);
  73. const [amountLoading, setAmountLoading] = useState(false);
  74. const [paymentLoading, setPaymentLoading] = useState(false);
  75. const [confirmLoading, setConfirmLoading] = useState(false);
  76. const [payMethods, setPayMethods] = useState([]);
  77. // 邀请相关状态
  78. const [affLink, setAffLink] = useState('');
  79. const [openTransfer, setOpenTransfer] = useState(false);
  80. const [transferAmount, setTransferAmount] = useState(0);
  81. // 预设充值额度选项
  82. const [presetAmounts, setPresetAmounts] = useState([
  83. { value: 5 },
  84. { value: 10 },
  85. { value: 30 },
  86. { value: 50 },
  87. { value: 100 },
  88. { value: 300 },
  89. { value: 500 },
  90. { value: 1000 },
  91. ]);
  92. const [selectedPreset, setSelectedPreset] = useState(null);
  93. const getUsername = () => {
  94. if (userState.user) {
  95. return userState.user.username;
  96. } else {
  97. return 'null';
  98. }
  99. };
  100. const getUserRole = () => {
  101. if (!userState.user) return t('普通用户');
  102. switch (userState.user.role) {
  103. case 100:
  104. return t('超级管理员');
  105. case 10:
  106. return t('管理员');
  107. case 0:
  108. default:
  109. return t('普通用户');
  110. }
  111. };
  112. const topUp = async () => {
  113. if (redemptionCode === '') {
  114. showInfo(t('请输入兑换码!'));
  115. return;
  116. }
  117. setIsSubmitting(true);
  118. try {
  119. const res = await API.post('/api/user/topup', {
  120. key: redemptionCode,
  121. });
  122. const { success, message, data } = res.data;
  123. if (success) {
  124. showSuccess(t('兑换成功!'));
  125. Modal.success({
  126. title: t('兑换成功!'),
  127. content: t('成功兑换额度:') + renderQuota(data),
  128. centered: true,
  129. });
  130. setUserQuota((quota) => {
  131. return quota + data;
  132. });
  133. if (userState.user) {
  134. const updatedUser = {
  135. ...userState.user,
  136. quota: userState.user.quota + data,
  137. };
  138. userDispatch({ type: 'login', payload: updatedUser });
  139. }
  140. setRedemptionCode('');
  141. } else {
  142. showError(message);
  143. }
  144. } catch (err) {
  145. showError(t('请求失败'));
  146. } finally {
  147. setIsSubmitting(false);
  148. }
  149. };
  150. const openTopUpLink = () => {
  151. if (!topUpLink) {
  152. showError(t('超级管理员未设置充值链接!'));
  153. return;
  154. }
  155. window.open(topUpLink, '_blank');
  156. };
  157. const preTopUp = async (payment) => {
  158. if (!enableOnlineTopUp) {
  159. showError(t('管理员未开启在线充值!'));
  160. return;
  161. }
  162. setPayWay(payment);
  163. setPaymentLoading(true);
  164. try {
  165. await getAmount();
  166. if (topUpCount < minTopUp) {
  167. showError(t('充值数量不能小于') + minTopUp);
  168. return;
  169. }
  170. setOpen(true);
  171. } catch (error) {
  172. showError(t('获取金额失败'));
  173. } finally {
  174. setPaymentLoading(false);
  175. }
  176. };
  177. const onlineTopUp = async () => {
  178. if (amount === 0) {
  179. await getAmount();
  180. }
  181. if (topUpCount < minTopUp) {
  182. showError('充值数量不能小于' + minTopUp);
  183. return;
  184. }
  185. setConfirmLoading(true);
  186. try {
  187. const res = await API.post('/api/user/pay', {
  188. amount: parseInt(topUpCount),
  189. top_up_code: topUpCode,
  190. payment_method: payWay,
  191. });
  192. if (res !== undefined) {
  193. const { message, data } = res.data;
  194. if (message === 'success') {
  195. let params = data;
  196. let url = res.data.url;
  197. let form = document.createElement('form');
  198. form.action = url;
  199. form.method = 'POST';
  200. let isSafari =
  201. navigator.userAgent.indexOf('Safari') > -1 &&
  202. navigator.userAgent.indexOf('Chrome') < 1;
  203. if (!isSafari) {
  204. form.target = '_blank';
  205. }
  206. for (let key in params) {
  207. let input = document.createElement('input');
  208. input.type = 'hidden';
  209. input.name = key;
  210. input.value = params[key];
  211. form.appendChild(input);
  212. }
  213. document.body.appendChild(form);
  214. form.submit();
  215. document.body.removeChild(form);
  216. } else {
  217. showError(data);
  218. }
  219. } else {
  220. showError(res);
  221. }
  222. } catch (err) {
  223. console.log(err);
  224. showError(t('支付请求失败'));
  225. } finally {
  226. setOpen(false);
  227. setConfirmLoading(false);
  228. }
  229. };
  230. const stripePreTopUp = async () => {
  231. if (!enableStripeTopUp) {
  232. showError(t('管理员未开启在线充值!'));
  233. return;
  234. }
  235. setPayWay('stripe');
  236. setPaymentLoading(true);
  237. try {
  238. await getStripeAmount();
  239. if (stripeTopUpCount < stripeMinTopUp) {
  240. showError(t('充值数量不能小于') + stripeMinTopUp);
  241. return;
  242. }
  243. setStripeOpen(true);
  244. } catch (error) {
  245. showError(t('获取金额失败'));
  246. } finally {
  247. setPaymentLoading(false);
  248. }
  249. };
  250. const onlineStripeTopUp = async () => {
  251. if (stripeAmount === 0) {
  252. await getStripeAmount();
  253. }
  254. if (stripeTopUpCount < stripeMinTopUp) {
  255. showError(t('充值数量不能小于') + stripeMinTopUp);
  256. return;
  257. }
  258. setConfirmLoading(true);
  259. try {
  260. const res = await API.post('/api/user/stripe/pay', {
  261. amount: parseInt(stripeTopUpCount),
  262. payment_method: 'stripe',
  263. });
  264. if (res !== undefined) {
  265. const { message, data } = res.data;
  266. if (message === 'success') {
  267. processStripeCallback(data);
  268. } else {
  269. showError(data);
  270. }
  271. } else {
  272. showError(res);
  273. }
  274. } catch (err) {
  275. console.log(err);
  276. showError(t('支付请求失败'));
  277. } finally {
  278. setStripeOpen(false);
  279. setConfirmLoading(false);
  280. }
  281. }
  282. const processStripeCallback = (data) => {
  283. window.open(data.pay_link, '_blank');
  284. };
  285. const creemPreTopUp = async (product) => {
  286. if (!enableCreemTopUp) {
  287. showError(t('管理员未开启 Creem 充值!'));
  288. return;
  289. }
  290. setSelectedCreemProduct(product);
  291. setCreemOpen(true);
  292. };
  293. const onlineCreemTopUp = async () => {
  294. if (!selectedCreemProduct) {
  295. showError(t('请选择产品'));
  296. return;
  297. }
  298. // Validate product has required fields
  299. if (!selectedCreemProduct.productId) {
  300. showError(t('产品配置错误,请联系管理员'));
  301. return;
  302. }
  303. setConfirmLoading(true);
  304. try {
  305. const res = await API.post('/api/user/creem/pay', {
  306. product_id: selectedCreemProduct.productId,
  307. payment_method: 'creem',
  308. });
  309. if (res !== undefined) {
  310. const { message, data } = res.data;
  311. if (message === 'success') {
  312. processCreemCallback(data);
  313. } else {
  314. showError(data);
  315. }
  316. } else {
  317. showError(res);
  318. }
  319. } catch (err) {
  320. console.log(err);
  321. showError(t('支付请求失败'));
  322. } finally {
  323. setCreemOpen(false);
  324. setConfirmLoading(false);
  325. }
  326. };
  327. const processCreemCallback = (data) => {
  328. // 与 Stripe 保持一致的实现方式
  329. window.open(data.checkout_url, '_blank');
  330. };
  331. const getUserQuota = async () => {
  332. setUserDataLoading(true);
  333. let res = await API.get(`/api/user/self`);
  334. const { success, message, data } = res.data;
  335. if (success) {
  336. setUserQuota(data.quota);
  337. userDispatch({ type: 'login', payload: data });
  338. } else {
  339. showError(message);
  340. }
  341. setUserDataLoading(false);
  342. };
  343. // 获取邀请链接
  344. const getAffLink = async () => {
  345. const res = await API.get('/api/user/aff');
  346. const { success, message, data } = res.data;
  347. if (success) {
  348. let link = `${window.location.origin}/register?aff=${data}`;
  349. setAffLink(link);
  350. } else {
  351. showError(message);
  352. }
  353. };
  354. // 划转邀请额度
  355. const transfer = async () => {
  356. if (transferAmount < getQuotaPerUnit()) {
  357. showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
  358. return;
  359. }
  360. const res = await API.post(`/api/user/aff_transfer`, {
  361. quota: transferAmount,
  362. });
  363. const { success, message } = res.data;
  364. if (success) {
  365. showSuccess(message);
  366. setOpenTransfer(false);
  367. getUserQuota().then();
  368. } else {
  369. showError(message);
  370. }
  371. };
  372. // 复制邀请链接
  373. const handleAffLinkClick = async () => {
  374. await copy(affLink);
  375. showSuccess(t('邀请链接已复制到剪切板'));
  376. };
  377. useEffect(() => {
  378. if (userState?.user?.id) {
  379. setUserDataLoading(false);
  380. setUserQuota(userState.user.quota);
  381. } else {
  382. getUserQuota().then();
  383. }
  384. getAffLink().then();
  385. setTransferAmount(getQuotaPerUnit());
  386. let payMethods = localStorage.getItem('pay_methods');
  387. try {
  388. payMethods = JSON.parse(payMethods);
  389. if (payMethods && payMethods.length > 0) {
  390. // 检查name和type是否为空
  391. payMethods = payMethods.filter((method) => {
  392. return method.name && method.type;
  393. });
  394. // 如果没有color,则设置默认颜色
  395. payMethods = payMethods.map((method) => {
  396. if (!method.color) {
  397. if (method.type === 'zfb') {
  398. method.color = 'rgba(var(--semi-blue-5), 1)';
  399. } else if (method.type === 'wx') {
  400. method.color = 'rgba(var(--semi-green-5), 1)';
  401. } else {
  402. method.color = 'rgba(var(--semi-primary-5), 1)';
  403. }
  404. }
  405. return method;
  406. });
  407. setPayMethods(payMethods);
  408. }
  409. } catch (e) {
  410. console.log(e);
  411. showError(t('支付方式配置错误, 请联系管理员'));
  412. }
  413. }, []);
  414. useEffect(() => {
  415. if (statusState?.status) {
  416. setMinTopUp(statusState.status.min_topup || 1);
  417. setTopUpCount(statusState.status.min_topup || 1);
  418. setTopUpLink(statusState.status.top_up_link || '');
  419. setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
  420. setPriceRatio(statusState.status.price || 1);
  421. setStripeMinTopUp(statusState.status.stripe_min_topup || 1);
  422. setStripeTopUpCount(statusState.status.stripe_min_topup || 1);
  423. setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
  424. // Creem settings
  425. setEnableCreemTopUp(statusState.status.enable_creem_topup || false);
  426. try {
  427. const products = JSON.parse(statusState.status.creem_products || '[]');
  428. setCreemProducts(products);
  429. } catch (e) {
  430. setCreemProducts([]);
  431. }
  432. }
  433. }, [statusState?.status]);
  434. const renderAmount = () => {
  435. return amount + ' ' + t('元');
  436. };
  437. const renderStripeAmount = () => {
  438. return stripeAmount + ' ' + t('元');
  439. };
  440. const getAmount = async (value) => {
  441. if (value === undefined) {
  442. value = topUpCount;
  443. }
  444. setAmountLoading(true);
  445. try {
  446. const res = await API.post('/api/user/amount', {
  447. amount: parseFloat(value),
  448. top_up_code: topUpCode,
  449. });
  450. if (res !== undefined) {
  451. const { message, data } = res.data;
  452. if (message === 'success') {
  453. setAmount(parseFloat(data));
  454. } else {
  455. setAmount(0);
  456. Toast.error({ content: '错误:' + data, id: 'getAmount' });
  457. }
  458. } else {
  459. showError(res);
  460. }
  461. } catch (err) {
  462. console.log(err);
  463. }
  464. setAmountLoading(false);
  465. };
  466. const getStripeAmount = async (value) => {
  467. if (value === undefined) {
  468. value = stripeTopUpCount
  469. }
  470. setAmountLoading(true);
  471. try {
  472. const res = await API.post('/api/user/stripe/amount', {
  473. amount: parseFloat(value),
  474. });
  475. if (res !== undefined) {
  476. const { message, data } = res.data;
  477. // showInfo(message);
  478. if (message === 'success') {
  479. setStripeAmount(parseFloat(data));
  480. } else {
  481. setStripeAmount(0);
  482. Toast.error({ content: '错误:' + data, id: 'getAmount' });
  483. }
  484. } else {
  485. showError(res);
  486. }
  487. } catch (err) {
  488. console.log(err);
  489. } finally {
  490. setAmountLoading(false);
  491. }
  492. }
  493. const handleCancel = () => {
  494. setOpen(false);
  495. };
  496. const handleStripeCancel = () => {
  497. setStripeOpen(false);
  498. };
  499. const handleCreemCancel = () => {
  500. setCreemOpen(false);
  501. setSelectedCreemProduct(null);
  502. };
  503. const handleTransferCancel = () => {
  504. setOpenTransfer(false);
  505. };
  506. // 选择预设充值额度
  507. const selectPresetAmount = (preset) => {
  508. setTopUpCount(preset.value);
  509. setSelectedPreset(preset.value);
  510. setAmount(preset.value * priceRatio);
  511. setStripeTopUpCount(preset.value);
  512. setStripeAmount(preset.value);
  513. };
  514. // 格式化大数字显示
  515. const formatLargeNumber = (num) => {
  516. return num.toString();
  517. };
  518. return (
  519. <div className='mx-auto relative min-h-screen lg:min-h-0 mt-[64px]'>
  520. {/* 划转模态框 */}
  521. <Modal
  522. title={
  523. <div className='flex items-center'>
  524. <CreditCard className='mr-2' size={18} />
  525. {t('划转邀请额度')}
  526. </div>
  527. }
  528. visible={openTransfer}
  529. onOk={transfer}
  530. onCancel={handleTransferCancel}
  531. maskClosable={false}
  532. size='small'
  533. centered
  534. >
  535. <div className='space-y-4'>
  536. <div>
  537. <Typography.Text strong className='block mb-2'>
  538. {t('可用邀请额度')}
  539. </Typography.Text>
  540. <Input
  541. value={renderQuota(userState?.user?.aff_quota)}
  542. disabled
  543. size='large'
  544. />
  545. </div>
  546. <div>
  547. <Typography.Text strong className='block mb-2'>
  548. {t('划转额度')} ({t('最低') + renderQuota(getQuotaPerUnit())})
  549. </Typography.Text>
  550. <InputNumber
  551. min={getQuotaPerUnit()}
  552. max={userState?.user?.aff_quota || 0}
  553. value={transferAmount}
  554. onChange={(value) => setTransferAmount(value)}
  555. size='large'
  556. className='w-full'
  557. />
  558. </div>
  559. </div>
  560. </Modal>
  561. {/* 充值确认模态框 */}
  562. <Modal
  563. title={
  564. <div className='flex items-center'>
  565. <CreditCard className='mr-2' size={18} />
  566. {t('充值确认')}
  567. </div>
  568. }
  569. visible={open}
  570. onOk={onlineTopUp}
  571. onCancel={handleCancel}
  572. maskClosable={false}
  573. size='small'
  574. centered
  575. confirmLoading={confirmLoading}
  576. >
  577. <div className='space-y-4'>
  578. <div className='flex justify-between items-center py-2'>
  579. <Text strong>{t('充值数量')}:</Text>
  580. <Text>{renderQuotaWithAmount(topUpCount)}</Text>
  581. </div>
  582. <div className='flex justify-between items-center py-2'>
  583. <Text strong>{t('实付金额')}:</Text>
  584. {amountLoading ? (
  585. <Skeleton.Title style={{ width: '60px', height: '16px' }} />
  586. ) : (
  587. <Text type='danger' strong>
  588. {renderAmount()}
  589. </Text>
  590. )}
  591. </div>
  592. <div className='flex justify-between items-center py-2'>
  593. <Text strong>{t('支付方式')}:</Text>
  594. <Text>
  595. {(() => {
  596. const payMethod = payMethods.find(
  597. (method) => method.type === payWay,
  598. );
  599. if (payMethod) {
  600. return (
  601. <div className='flex items-center'>
  602. {payMethod.type === 'zfb' ? (
  603. <SiAlipay className='mr-1' size={16} />
  604. ) : payMethod.type === 'wx' ? (
  605. <SiWechat className='mr-1' size={16} />
  606. ) : (
  607. <CreditCard className='mr-1' size={16} />
  608. )}
  609. {payMethod.name}
  610. </div>
  611. );
  612. } else {
  613. // 默认充值方式
  614. return payWay === 'zfb' ? (
  615. <div className='flex items-center'>
  616. <SiAlipay className='mr-1' size={16} />
  617. {t('支付宝')}
  618. </div>
  619. ) : (
  620. <div className='flex items-center'>
  621. <SiWechat className='mr-1' size={16} />
  622. {t('微信')}
  623. </div>
  624. );
  625. }
  626. })()}
  627. </Text>
  628. </div>
  629. </div>
  630. </Modal>
  631. <Modal
  632. title={t('确定要充值 $')}
  633. visible={stripeOpen}
  634. onOk={onlineStripeTopUp}
  635. onCancel={handleStripeCancel}
  636. maskClosable={false}
  637. size='small'
  638. centered
  639. confirmLoading={confirmLoading}
  640. >
  641. <p>
  642. {t('充值数量')}:{stripeTopUpCount}
  643. </p>
  644. <p>
  645. {t('实付金额')}:{renderStripeAmount()}
  646. </p>
  647. <p>{t('是否确认充值?')}</p>
  648. </Modal>
  649. <Modal
  650. title={t('确定要充值 $')}
  651. visible={creemOpen}
  652. onOk={onlineCreemTopUp}
  653. onCancel={handleCreemCancel}
  654. maskClosable={false}
  655. size='small'
  656. centered
  657. confirmLoading={confirmLoading}
  658. >
  659. {selectedCreemProduct && (
  660. <>
  661. <p>
  662. {t('产品名称')}:{selectedCreemProduct.name}
  663. </p>
  664. <p>
  665. {t('价格')}:{selectedCreemProduct.currency === 'EUR' ? '€' : '$'}{selectedCreemProduct.price}
  666. </p>
  667. <p>
  668. {t('充值额度')}:{selectedCreemProduct.quota}
  669. </p>
  670. <p>{t('是否确认充值?')}</p>
  671. </>
  672. )}
  673. </Modal>
  674. <div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
  675. {/* 左侧充值区域 */}
  676. <div className='lg:col-span-7 space-y-6 w-full'>
  677. {/* 在线充值卡片 */}
  678. <Card
  679. className='!rounded-2xl'
  680. shadows='always'
  681. bordered={false}
  682. header={
  683. <div className='px-5 py-4 pb-0'>
  684. <div className='flex items-center justify-between'>
  685. <div className='flex items-center'>
  686. <Avatar
  687. className='mr-3 shadow-md flex-shrink-0'
  688. color='blue'
  689. >
  690. <CreditCard size={24} />
  691. </Avatar>
  692. <div>
  693. <Title heading={5} style={{ margin: 0 }}>
  694. {t('在线充值')}
  695. </Title>
  696. <Text type='tertiary' className='text-sm'>
  697. {t('快速方便的充值方式')}
  698. </Text>
  699. </div>
  700. </div>
  701. <div className='flex items-center'>
  702. {userDataLoading ? (
  703. <Skeleton.Paragraph style={{ width: '120px' }} rows={1} />
  704. ) : (
  705. <Text type='tertiary' className='hidden sm:block'>
  706. <div className='flex items-center'>
  707. <User size={14} className='mr-1' />
  708. <span className='hidden md:inline'>
  709. {getUsername()} ({getUserRole()})
  710. </span>
  711. <span className='md:hidden'>{getUsername()}</span>
  712. </div>
  713. </Text>
  714. )}
  715. </div>
  716. </div>
  717. </div>
  718. }
  719. >
  720. <div className='space-y-4'>
  721. {/* 账户余额信息 */}
  722. <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-2'>
  723. <Card className='!rounded-2xl'>
  724. <Text type='tertiary' className='mb-1'>
  725. {t('当前余额')}
  726. </Text>
  727. {userDataLoading ? (
  728. <Skeleton.Title
  729. style={{ width: '100px', height: '30px' }}
  730. />
  731. ) : (
  732. <div className='text-xl font-semibold mt-2'>
  733. {renderQuota(userState?.user?.quota || userQuota)}
  734. </div>
  735. )}
  736. </Card>
  737. <Card className='!rounded-2xl'>
  738. <Text type='tertiary' className='mb-1'>
  739. {t('历史消耗')}
  740. </Text>
  741. {userDataLoading ? (
  742. <Skeleton.Title
  743. style={{ width: '100px', height: '30px' }}
  744. />
  745. ) : (
  746. <div className='text-xl font-semibold mt-2'>
  747. {renderQuota(userState?.user?.used_quota || 0)}
  748. </div>
  749. )}
  750. </Card>
  751. </div>
  752. {enableOnlineTopUp && (
  753. <>
  754. {/* 预设充值额度卡片网格 */}
  755. <div>
  756. <Text strong className='block mb-3'>
  757. {t('选择充值额度')}
  758. </Text>
  759. <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3'>
  760. {presetAmounts.map((preset, index) => (
  761. <Card
  762. key={index}
  763. onClick={() => selectPresetAmount(preset)}
  764. className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${selectedPreset === preset.value
  765. ? 'border-blue-500'
  766. : 'border-gray-200 hover:border-gray-300'
  767. }`}
  768. bodyStyle={{ textAlign: 'center' }}
  769. >
  770. <div className='font-medium text-lg flex items-center justify-center mb-1'>
  771. <Coins size={16} className='mr-0.5' />
  772. {formatLargeNumber(preset.value)}
  773. </div>
  774. <div className='text-xs text-gray-500'>
  775. {t('实付')} ¥
  776. {(preset.value * priceRatio).toFixed(2)}
  777. </div>
  778. </Card>
  779. ))}
  780. </div>
  781. </div>
  782. {/* 桌面端显示的自定义金额和支付按钮 */}
  783. <div className='hidden md:block space-y-4'>
  784. <Divider style={{ margin: '24px 0' }}>
  785. <Text className='text-sm font-medium'>
  786. {t('或输入自定义金额')}
  787. </Text>
  788. </Divider>
  789. <div>
  790. <div className='flex justify-between mb-2'>
  791. <Text strong>{t('充值数量')}</Text>
  792. {amountLoading ? (
  793. <Skeleton.Title
  794. style={{ width: '80px', height: '16px' }}
  795. />
  796. ) : (
  797. <Text type='tertiary'>
  798. {t('实付金额:') + renderAmount()}
  799. </Text>
  800. )}
  801. </div>
  802. <InputNumber
  803. disabled={!enableOnlineTopUp}
  804. placeholder={
  805. t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
  806. }
  807. value={topUpCount}
  808. min={minTopUp}
  809. max={999999999}
  810. step={1}
  811. precision={0}
  812. onChange={async (value) => {
  813. if (value && value >= 1) {
  814. setTopUpCount(value);
  815. setSelectedPreset(null);
  816. await getAmount(value);
  817. }
  818. }}
  819. onBlur={(e) => {
  820. const value = parseInt(e.target.value);
  821. if (!value || value < 1) {
  822. setTopUpCount(1);
  823. getAmount(1);
  824. }
  825. }}
  826. size='large'
  827. className='w-full'
  828. formatter={(value) => (value ? `${value}` : '')}
  829. parser={(value) =>
  830. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  831. }
  832. />
  833. </div>
  834. <div>
  835. <Text strong className='block mb-3'>
  836. {t('选择支付方式')}
  837. </Text>
  838. {payMethods.length === 2 ? (
  839. <div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
  840. {payMethods.map((payMethod) => (
  841. <Button
  842. key={payMethod.type}
  843. type='primary'
  844. onClick={() => preTopUp(payMethod.type)}
  845. size='large'
  846. disabled={!enableOnlineTopUp}
  847. loading={paymentLoading && payWay === payMethod.type}
  848. icon={
  849. payMethod.type === 'zfb' ? (
  850. <SiAlipay size={16} />
  851. ) : payMethod.type === 'wx' ? (
  852. <SiWechat size={16} />
  853. ) : (
  854. <CreditCard size={16} />
  855. )
  856. }
  857. style={{
  858. height: '40px',
  859. color: payMethod.color,
  860. }}
  861. className='transition-all hover:shadow-md w-full'
  862. >
  863. <span className='ml-1'>{payMethod.name}</span>
  864. </Button>
  865. ))}
  866. </div>
  867. ) : payMethods.length === 3 ? (
  868. <div className='grid grid-cols-1 sm:grid-cols-3 gap-3'>
  869. {payMethods.map((payMethod) => (
  870. <Button
  871. key={payMethod.type}
  872. type='primary'
  873. onClick={() => preTopUp(payMethod.type)}
  874. size='large'
  875. disabled={!enableOnlineTopUp}
  876. loading={paymentLoading && payWay === payMethod.type}
  877. icon={
  878. payMethod.type === 'zfb' ? (
  879. <SiAlipay size={16} />
  880. ) : payMethod.type === 'wx' ? (
  881. <SiWechat size={16} />
  882. ) : (
  883. <CreditCard size={16} />
  884. )
  885. }
  886. style={{
  887. height: '40px',
  888. color: payMethod.color,
  889. }}
  890. className='transition-all hover:shadow-md w-full'
  891. >
  892. <span className='ml-1'>{payMethod.name}</span>
  893. </Button>
  894. ))}
  895. </div>
  896. ) : payMethods.length > 3 ? (
  897. <div className='grid grid-cols-2 sm:grid-cols-4 gap-3'>
  898. {payMethods.map((payMethod) => (
  899. <Card
  900. key={payMethod.type}
  901. onClick={() => preTopUp(payMethod.type)}
  902. disabled={!enableOnlineTopUp}
  903. className={`cursor-pointer !rounded-xl p-0 transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type
  904. ? 'border-blue-400'
  905. : 'border-gray-200 hover:border-gray-300'
  906. }`}
  907. bodyStyle={{
  908. padding: '10px',
  909. textAlign: 'center',
  910. opacity: !enableOnlineTopUp ? 0.5 : 1
  911. }}
  912. >
  913. {paymentLoading && payWay === payMethod.type ? (
  914. <div className='flex flex-col items-center justify-center h-full'>
  915. <div className='mb-1'>
  916. <div className='animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500'></div>
  917. </div>
  918. <div className='text-xs text-gray-500'>{t('处理中')}</div>
  919. </div>
  920. ) : (
  921. <>
  922. <div className='flex items-center justify-center mb-1'>
  923. {payMethod.type === 'zfb' ? (
  924. <SiAlipay size={20} color={payMethod.color} />
  925. ) : payMethod.type === 'wx' ? (
  926. <SiWechat size={20} color={payMethod.color} />
  927. ) : (
  928. <CreditCard size={20} color={payMethod.color} />
  929. )}
  930. </div>
  931. <div className='text-sm font-medium'>{payMethod.name}</div>
  932. </>
  933. )}
  934. </Card>
  935. ))}
  936. </div>
  937. ) : (
  938. <div className='grid grid-cols-1 gap-3'>
  939. {payMethods.map((payMethod) => (
  940. <Button
  941. key={payMethod.type}
  942. type='primary'
  943. onClick={() => preTopUp(payMethod.type)}
  944. size='large'
  945. disabled={!enableOnlineTopUp}
  946. loading={paymentLoading && payWay === payMethod.type}
  947. icon={
  948. payMethod.type === 'zfb' ? (
  949. <SiAlipay size={16} />
  950. ) : payMethod.type === 'wx' ? (
  951. <SiWechat size={16} />
  952. ) : (
  953. <CreditCard size={16} />
  954. )
  955. }
  956. style={{
  957. height: '40px',
  958. color: payMethod.color,
  959. }}
  960. className='transition-all hover:shadow-md w-full'
  961. >
  962. <span className='ml-1'>{payMethod.name}</span>
  963. </Button>
  964. ))}
  965. </div>
  966. )}
  967. </div>
  968. </div>
  969. </>
  970. )}
  971. {!enableOnlineTopUp && !enableStripeTopUp && !enableCreemTopUp && (
  972. <Banner
  973. type='warning'
  974. description={t(
  975. '管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。',
  976. )}
  977. closeIcon={null}
  978. className='!rounded-2xl'
  979. />
  980. )}
  981. {enableStripeTopUp && (
  982. <>
  983. {/* 桌面端显示的自定义金额和支付按钮 */}
  984. <div className='hidden md:block space-y-4'>
  985. <Divider style={{ margin: '24px 0' }}>
  986. <Text className='text-sm font-medium'>
  987. {t(!enableOnlineTopUp ? '或输入自定义金额' : 'Stripe')}
  988. </Text>
  989. </Divider>
  990. <div>
  991. <div className='flex justify-between mb-2'>
  992. <Text strong>{t('充值数量')}</Text>
  993. {amountLoading ? (
  994. <Skeleton.Title
  995. style={{ width: '80px', height: '16px' }}
  996. />
  997. ) : (
  998. <Text type='tertiary'>
  999. {t('实付金额:') + renderStripeAmount()}
  1000. </Text>
  1001. )}
  1002. </div>
  1003. <InputNumber
  1004. disabled={!enableStripeTopUp}
  1005. placeholder={
  1006. t('充值数量,最低 ') + renderQuotaWithAmount(stripeMinTopUp)
  1007. }
  1008. value={stripeTopUpCount}
  1009. min={stripeMinTopUp}
  1010. max={999999999}
  1011. step={1}
  1012. precision={0}
  1013. onChange={async (value) => {
  1014. if (value && value >= 1) {
  1015. setStripeTopUpCount(value);
  1016. setSelectedPreset(null);
  1017. await getStripeAmount(value);
  1018. }
  1019. }}
  1020. onBlur={(e) => {
  1021. const value = parseInt(e.target.value);
  1022. if (!value || value < 1) {
  1023. setStripeTopUpCount(1);
  1024. getStripeAmount(1);
  1025. }
  1026. }}
  1027. size='large'
  1028. className='w-full'
  1029. formatter={(value) => (value ? `${value}` : '')}
  1030. parser={(value) =>
  1031. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  1032. }
  1033. />
  1034. </div>
  1035. <div>
  1036. <Text strong className='block mb-3'>
  1037. {t('选择支付方式')}
  1038. </Text>
  1039. <div className='grid grid-cols-1 gap-3'>
  1040. <Button
  1041. key='stripe'
  1042. type='primary'
  1043. onClick={() => stripePreTopUp()}
  1044. size='large'
  1045. disabled={!enableStripeTopUp}
  1046. loading={paymentLoading && payWay === 'stripe'}
  1047. icon={<CreditCard size={16} />}
  1048. style={{
  1049. height: '40px',
  1050. color: '#b161fe',
  1051. }}
  1052. className='transition-all hover:shadow-md w-full'
  1053. >
  1054. <span className='ml-1'>Stripe</span>
  1055. </Button>
  1056. </div>
  1057. </div>
  1058. </div>
  1059. {/* 移动端 Stripe 充值区域 */}
  1060. <div className='md:hidden space-y-4'>
  1061. <Divider style={{ margin: '24px 0' }}>
  1062. <Text className='text-sm font-medium'>
  1063. {t('Stripe 充值')}
  1064. </Text>
  1065. </Divider>
  1066. <div>
  1067. <div className='flex justify-between mb-2'>
  1068. <Text strong>{t('充值数量')}</Text>
  1069. {amountLoading ? (
  1070. <Skeleton.Title
  1071. style={{ width: '80px', height: '16px' }}
  1072. />
  1073. ) : (
  1074. <Text type='tertiary'>
  1075. {t('实付金额:') + renderStripeAmount()}
  1076. </Text>
  1077. )}
  1078. </div>
  1079. <InputNumber
  1080. disabled={!enableStripeTopUp}
  1081. placeholder={
  1082. t('充值数量,最低 ') + renderQuotaWithAmount(stripeMinTopUp)
  1083. }
  1084. value={stripeTopUpCount}
  1085. min={stripeMinTopUp}
  1086. max={999999999}
  1087. step={1}
  1088. precision={0}
  1089. onChange={async (value) => {
  1090. if (value && value >= 1) {
  1091. setStripeTopUpCount(value);
  1092. setSelectedPreset(null);
  1093. await getStripeAmount(value);
  1094. }
  1095. }}
  1096. onBlur={(e) => {
  1097. const value = parseInt(e.target.value);
  1098. if (!value || value < 1) {
  1099. setStripeTopUpCount(1);
  1100. getStripeAmount(1);
  1101. }
  1102. }}
  1103. className='w-full'
  1104. formatter={(value) => (value ? `${value}` : '')}
  1105. parser={(value) =>
  1106. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  1107. }
  1108. />
  1109. </div>
  1110. <div>
  1111. <Button
  1112. type='primary'
  1113. onClick={() => stripePreTopUp()}
  1114. disabled={!enableStripeTopUp}
  1115. loading={paymentLoading && payWay === 'stripe'}
  1116. icon={<CreditCard size={16} />}
  1117. style={{
  1118. height: '40px',
  1119. color: '#b161fe',
  1120. }}
  1121. className='transition-all hover:shadow-md w-full'
  1122. >
  1123. <span className='ml-1'>Stripe</span>
  1124. </Button>
  1125. </div>
  1126. </div>
  1127. </>
  1128. )}
  1129. {enableCreemTopUp && creemProducts.length > 0 && (
  1130. <>
  1131. <div className='hidden md:block space-y-4'>
  1132. <Divider style={{ margin: '24px 0' }}>
  1133. <Text className='text-sm font-medium'>Creem</Text>
  1134. </Divider>
  1135. <div>
  1136. <Text strong className='block mb-3'>
  1137. {t('选择充值套餐')}
  1138. </Text>
  1139. <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3'>
  1140. {creemProducts.map((product, index) => (
  1141. <Card
  1142. key={index}
  1143. onClick={() => creemPreTopUp(product)}
  1144. className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300'
  1145. bodyStyle={{ textAlign: 'center', padding: '16px' }}
  1146. >
  1147. <div className='font-medium text-lg mb-2'>
  1148. {product.name}
  1149. </div>
  1150. <div className='text-sm text-gray-600 mb-2'>
  1151. {t('充值额度')}: {product.quota}
  1152. </div>
  1153. <div className='text-lg font-semibold text-blue-600'>
  1154. {product.currency === 'EUR' ? '€' : '$'}{product.price}
  1155. </div>
  1156. </Card>
  1157. ))}
  1158. </div>
  1159. </div>
  1160. </div>
  1161. {/* 移动端 Creem 充值区域 */}
  1162. <div className='md:hidden space-y-4'>
  1163. <Divider style={{ margin: '24px 0' }}>
  1164. <Text className='text-sm font-medium'>Creem</Text>
  1165. </Divider>
  1166. <div>
  1167. <Text strong className='block mb-3'>
  1168. {t('选择充值套餐')}
  1169. </Text>
  1170. <div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
  1171. {creemProducts.map((product, index) => (
  1172. <Card
  1173. key={index}
  1174. onClick={() => creemPreTopUp(product)}
  1175. className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300'
  1176. bodyStyle={{ textAlign: 'center', padding: '16px' }}
  1177. >
  1178. <div className='font-medium text-lg mb-2'>
  1179. {product.name}
  1180. </div>
  1181. <div className='text-sm text-gray-600 mb-2'>
  1182. {t('充值额度')}: {product.quota}
  1183. </div>
  1184. <div className='text-lg font-semibold text-blue-600'>
  1185. {product.currency === 'EUR' ? '€' : '$'}{product.price}
  1186. </div>
  1187. </Card>
  1188. ))}
  1189. </div>
  1190. </div>
  1191. </div>
  1192. </>
  1193. )}
  1194. <Divider style={{ margin: '24px 0' }}>
  1195. <Text className='text-sm font-medium'>{t('兑换码充值')}</Text>
  1196. </Divider>
  1197. <Card className='!rounded-2xl'>
  1198. <div className='flex items-start mb-4'>
  1199. <Gift size={16} className='mr-2 mt-0.5' />
  1200. <Text strong>{t('使用兑换码快速充值')}</Text>
  1201. </div>
  1202. <div className='mb-4'>
  1203. <Input
  1204. placeholder={t('请输入兑换码')}
  1205. value={redemptionCode}
  1206. onChange={(value) => setRedemptionCode(value)}
  1207. size='large'
  1208. />
  1209. </div>
  1210. <div className='flex flex-col sm:flex-row gap-3'>
  1211. {topUpLink && (
  1212. <Button
  1213. type='secondary'
  1214. onClick={openTopUpLink}
  1215. size='large'
  1216. className='flex-1'
  1217. icon={<LinkIcon size={16} />}
  1218. style={{ height: '40px' }}
  1219. >
  1220. {t('获取兑换码')}
  1221. </Button>
  1222. )}
  1223. <Button
  1224. type='primary'
  1225. onClick={topUp}
  1226. disabled={isSubmitting || !redemptionCode}
  1227. loading={isSubmitting}
  1228. size='large'
  1229. className='flex-1'
  1230. style={{ height: '40px' }}
  1231. >
  1232. {isSubmitting ? t('兑换中...') : t('兑换')}
  1233. </Button>
  1234. </div>
  1235. </Card>
  1236. </div>
  1237. </Card>
  1238. </div>
  1239. {/* 右侧邀请信息卡片 */}
  1240. <div className='lg:col-span-5'>
  1241. <Card
  1242. className='!rounded-2xl'
  1243. shadows='always'
  1244. bordered={false}
  1245. header={
  1246. <div className='px-5 py-4 pb-0'>
  1247. <div className='flex items-center justify-between'>
  1248. <div className='flex items-center'>
  1249. <Avatar
  1250. className='mr-3 shadow-md flex-shrink-0'
  1251. color='green'
  1252. >
  1253. <Users size={24} />
  1254. </Avatar>
  1255. <div>
  1256. <Title heading={5} style={{ margin: 0 }}>
  1257. {t('邀请奖励')}
  1258. </Title>
  1259. <Text type='tertiary' className='text-sm'>
  1260. {t('邀请好友获得额外奖励')}
  1261. </Text>
  1262. </div>
  1263. </div>
  1264. </div>
  1265. </div>
  1266. }
  1267. >
  1268. <div className='space-y-6'>
  1269. <div className='grid grid-cols-1 gap-4'>
  1270. <Card className='!rounded-2xl'>
  1271. <div className='flex justify-between items-center'>
  1272. <Text type='tertiary'>{t('待使用收益')}</Text>
  1273. <Button
  1274. type='primary'
  1275. theme='solid'
  1276. size='small'
  1277. disabled={
  1278. !userState?.user?.aff_quota ||
  1279. userState?.user?.aff_quota <= 0
  1280. }
  1281. onClick={() => setOpenTransfer(true)}
  1282. >
  1283. {t('划转到余额')}
  1284. </Button>
  1285. </div>
  1286. <div className='text-2xl font-semibold mt-2'>
  1287. {renderQuota(userState?.user?.aff_quota || 0)}
  1288. </div>
  1289. </Card>
  1290. <div className='grid grid-cols-2 gap-4'>
  1291. <Card className='!rounded-2xl'>
  1292. <Text type='tertiary'>{t('总收益')}</Text>
  1293. <div className='text-xl font-semibold mt-2'>
  1294. {renderQuota(userState?.user?.aff_history_quota || 0)}
  1295. </div>
  1296. </Card>
  1297. <Card className='!rounded-2xl'>
  1298. <Text type='tertiary'>{t('邀请人数')}</Text>
  1299. <div className='text-xl font-semibold mt-2 flex items-center'>
  1300. <Users size={16} className='mr-1' />
  1301. {userState?.user?.aff_count || 0}
  1302. </div>
  1303. </Card>
  1304. </div>
  1305. </div>
  1306. <div className='space-y-4'>
  1307. <Title heading={6}>{t('邀请链接')}</Title>
  1308. <Input
  1309. value={affLink}
  1310. readonly
  1311. size='large'
  1312. suffix={
  1313. <Button
  1314. type='primary'
  1315. theme='light'
  1316. onClick={handleAffLinkClick}
  1317. icon={<Copy size={14} />}
  1318. >
  1319. {t('复制')}
  1320. </Button>
  1321. }
  1322. />
  1323. <div className='mt-4'>
  1324. <Card className='!rounded-2xl'>
  1325. <div className='space-y-4'>
  1326. <div className='flex items-start'>
  1327. <div className='w-1.5 h-1.5 rounded-full bg-blue-500 mt-2 mr-3 flex-shrink-0'></div>
  1328. <Text type='tertiary' className='text-sm leading-6'>
  1329. {t('邀请好友注册,好友充值后您可获得相应奖励')}
  1330. </Text>
  1331. </div>
  1332. <div className='flex items-start'>
  1333. <div className='w-1.5 h-1.5 rounded-full bg-green-500 mt-2 mr-3 flex-shrink-0'></div>
  1334. <Text type='tertiary' className='text-sm leading-6'>
  1335. {t('通过划转功能将奖励额度转入到您的账户余额中')}
  1336. </Text>
  1337. </div>
  1338. <div className='flex items-start'>
  1339. <div className='w-1.5 h-1.5 rounded-full bg-purple-500 mt-2 mr-3 flex-shrink-0'></div>
  1340. <Text type='tertiary' className='text-sm leading-6'>
  1341. {t('邀请的好友越多,获得的奖励越多')}
  1342. </Text>
  1343. </div>
  1344. </div>
  1345. </Card>
  1346. </div>
  1347. </div>
  1348. </div>
  1349. </Card>
  1350. </div>
  1351. </div>
  1352. {/* 移动端底部间距,避免内容被固定区域遮挡 */}
  1353. {enableOnlineTopUp && (
  1354. <div className='md:hidden h-32'></div>
  1355. )}
  1356. {/* 移动端底部固定的自定义金额和支付区域 - 仅限在线充值 */}
  1357. {enableOnlineTopUp && (
  1358. <div
  1359. className='md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50'
  1360. style={{ background: 'var(--semi-color-bg-0)' }}
  1361. >
  1362. <div className='space-y-4'>
  1363. <div>
  1364. <div className='flex justify-between mb-2'>
  1365. <Text strong>{t('充值数量')}</Text>
  1366. {amountLoading ? (
  1367. <Skeleton.Title style={{ width: '80px', height: '16px' }} />
  1368. ) : (
  1369. <Text type='tertiary'>
  1370. {t('实付金额:') + renderAmount()}
  1371. </Text>
  1372. )}
  1373. </div>
  1374. <InputNumber
  1375. disabled={!enableOnlineTopUp}
  1376. placeholder={
  1377. t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
  1378. }
  1379. value={topUpCount}
  1380. min={minTopUp}
  1381. max={999999999}
  1382. step={1}
  1383. precision={0}
  1384. onChange={async (value) => {
  1385. if (value && value >= 1) {
  1386. setTopUpCount(value);
  1387. setSelectedPreset(null);
  1388. await getAmount(value);
  1389. }
  1390. }}
  1391. onBlur={(e) => {
  1392. const value = parseInt(e.target.value);
  1393. if (!value || value < 1) {
  1394. setTopUpCount(1);
  1395. getAmount(1);
  1396. }
  1397. }}
  1398. className='w-full'
  1399. formatter={(value) => (value ? `${value}` : '')}
  1400. parser={(value) =>
  1401. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  1402. }
  1403. />
  1404. </div>
  1405. <div>
  1406. {payMethods.length === 2 ? (
  1407. <div className='grid grid-cols-2 gap-3'>
  1408. {payMethods.map((payMethod) => (
  1409. <Button
  1410. key={payMethod.type}
  1411. type='primary'
  1412. onClick={() => preTopUp(payMethod.type)}
  1413. disabled={!enableOnlineTopUp}
  1414. loading={paymentLoading && payWay === payMethod.type}
  1415. icon={
  1416. payMethod.type === 'zfb' ? (
  1417. <SiAlipay size={16} />
  1418. ) : payMethod.type === 'wx' ? (
  1419. <SiWechat size={16} />
  1420. ) : (
  1421. <CreditCard size={16} />
  1422. )
  1423. }
  1424. style={{
  1425. color: payMethod.color,
  1426. }}
  1427. className='h-10'
  1428. >
  1429. <span className='ml-1'>{payMethod.name}</span>
  1430. </Button>
  1431. ))}
  1432. </div>
  1433. ) : (
  1434. <div className='grid grid-cols-4 gap-2'>
  1435. {payMethods.map((payMethod) => (
  1436. <Card
  1437. key={payMethod.type}
  1438. onClick={() => preTopUp(payMethod.type)}
  1439. disabled={!enableOnlineTopUp}
  1440. className={`cursor-pointer !rounded-xl p-0 transition-all ${paymentLoading && payWay === payMethod.type
  1441. ? 'border-blue-400'
  1442. : 'border-gray-200'
  1443. }`}
  1444. bodyStyle={{
  1445. padding: '8px',
  1446. textAlign: 'center',
  1447. opacity: !enableOnlineTopUp ? 0.5 : 1
  1448. }}
  1449. >
  1450. {paymentLoading && payWay === payMethod.type ? (
  1451. <div className='animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500 mx-auto'></div>
  1452. ) : (
  1453. <>
  1454. <div className='flex justify-center'>
  1455. {payMethod.type === 'zfb' ? (
  1456. <SiAlipay size={18} color={payMethod.color} />
  1457. ) : payMethod.type === 'wx' ? (
  1458. <SiWechat size={18} color={payMethod.color} />
  1459. ) : (
  1460. <CreditCard size={18} color={payMethod.color} />
  1461. )}
  1462. </div>
  1463. <div className='text-xs mt-1'>{payMethod.name}</div>
  1464. </>
  1465. )}
  1466. </Card>
  1467. ))}
  1468. </div>
  1469. )}
  1470. </div>
  1471. </div>
  1472. </div>
  1473. )}
  1474. </div>
  1475. );
  1476. };
  1477. export default TopUp;