index.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101
  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 [userQuota, setUserQuota] = useState(0);
  60. const [isSubmitting, setIsSubmitting] = useState(false);
  61. const [open, setOpen] = useState(false);
  62. const [payWay, setPayWay] = useState('');
  63. const [userDataLoading, setUserDataLoading] = useState(true);
  64. const [amountLoading, setAmountLoading] = useState(false);
  65. const [paymentLoading, setPaymentLoading] = useState(false);
  66. const [confirmLoading, setConfirmLoading] = useState(false);
  67. const [payMethods, setPayMethods] = useState([]);
  68. // 邀请相关状态
  69. const [affLink, setAffLink] = useState('');
  70. const [openTransfer, setOpenTransfer] = useState(false);
  71. const [transferAmount, setTransferAmount] = useState(0);
  72. // 预设充值额度选项
  73. const [presetAmounts, setPresetAmounts] = useState([
  74. { value: 5 },
  75. { value: 10 },
  76. { value: 30 },
  77. { value: 50 },
  78. { value: 100 },
  79. { value: 300 },
  80. { value: 500 },
  81. { value: 1000 },
  82. ]);
  83. const [selectedPreset, setSelectedPreset] = useState(null);
  84. const getUsername = () => {
  85. if (userState.user) {
  86. return userState.user.username;
  87. } else {
  88. return 'null';
  89. }
  90. };
  91. const getUserRole = () => {
  92. if (!userState.user) return t('普通用户');
  93. switch (userState.user.role) {
  94. case 100:
  95. return t('超级管理员');
  96. case 10:
  97. return t('管理员');
  98. case 0:
  99. default:
  100. return t('普通用户');
  101. }
  102. };
  103. const topUp = async () => {
  104. if (redemptionCode === '') {
  105. showInfo(t('请输入兑换码!'));
  106. return;
  107. }
  108. setIsSubmitting(true);
  109. try {
  110. const res = await API.post('/api/user/topup', {
  111. key: redemptionCode,
  112. });
  113. const { success, message, data } = res.data;
  114. if (success) {
  115. showSuccess(t('兑换成功!'));
  116. Modal.success({
  117. title: t('兑换成功!'),
  118. content: t('成功兑换额度:') + renderQuota(data),
  119. centered: true,
  120. });
  121. setUserQuota((quota) => {
  122. return quota + data;
  123. });
  124. if (userState.user) {
  125. const updatedUser = {
  126. ...userState.user,
  127. quota: userState.user.quota + data,
  128. };
  129. userDispatch({ type: 'login', payload: updatedUser });
  130. }
  131. setRedemptionCode('');
  132. } else {
  133. showError(message);
  134. }
  135. } catch (err) {
  136. showError(t('请求失败'));
  137. } finally {
  138. setIsSubmitting(false);
  139. }
  140. };
  141. const openTopUpLink = () => {
  142. if (!topUpLink) {
  143. showError(t('超级管理员未设置充值链接!'));
  144. return;
  145. }
  146. window.open(topUpLink, '_blank');
  147. };
  148. const preTopUp = async (payment) => {
  149. if (!enableOnlineTopUp) {
  150. showError(t('管理员未开启在线充值!'));
  151. return;
  152. }
  153. setPaymentLoading(true);
  154. try {
  155. await getAmount();
  156. if (topUpCount < minTopUp) {
  157. showError(t('充值数量不能小于') + minTopUp);
  158. return;
  159. }
  160. setPayWay(payment);
  161. setOpen(true);
  162. } catch (error) {
  163. showError(t('获取金额失败'));
  164. } finally {
  165. setPaymentLoading(false);
  166. }
  167. };
  168. const onlineTopUp = async () => {
  169. if (amount === 0) {
  170. await getAmount();
  171. }
  172. if (topUpCount < minTopUp) {
  173. showError('充值数量不能小于' + minTopUp);
  174. return;
  175. }
  176. setConfirmLoading(true);
  177. setOpen(false);
  178. try {
  179. const res = await API.post('/api/user/pay', {
  180. amount: parseInt(topUpCount),
  181. top_up_code: topUpCode,
  182. payment_method: payWay,
  183. });
  184. if (res !== undefined) {
  185. const { message, data } = res.data;
  186. if (message === 'success') {
  187. let params = data;
  188. let url = res.data.url;
  189. let form = document.createElement('form');
  190. form.action = url;
  191. form.method = 'POST';
  192. let isSafari =
  193. navigator.userAgent.indexOf('Safari') > -1 &&
  194. navigator.userAgent.indexOf('Chrome') < 1;
  195. if (!isSafari) {
  196. form.target = '_blank';
  197. }
  198. for (let key in params) {
  199. let input = document.createElement('input');
  200. input.type = 'hidden';
  201. input.name = key;
  202. input.value = params[key];
  203. form.appendChild(input);
  204. }
  205. document.body.appendChild(form);
  206. form.submit();
  207. document.body.removeChild(form);
  208. } else {
  209. showError(data);
  210. }
  211. } else {
  212. showError(res);
  213. }
  214. } catch (err) {
  215. console.log(err);
  216. showError(t('支付请求失败'));
  217. } finally {
  218. setConfirmLoading(false);
  219. }
  220. };
  221. const getUserQuota = async () => {
  222. setUserDataLoading(true);
  223. let res = await API.get(`/api/user/self`);
  224. const { success, message, data } = res.data;
  225. if (success) {
  226. setUserQuota(data.quota);
  227. userDispatch({ type: 'login', payload: data });
  228. } else {
  229. showError(message);
  230. }
  231. setUserDataLoading(false);
  232. };
  233. // 获取邀请链接
  234. const getAffLink = async () => {
  235. const res = await API.get('/api/user/aff');
  236. const { success, message, data } = res.data;
  237. if (success) {
  238. let link = `${window.location.origin}/register?aff=${data}`;
  239. setAffLink(link);
  240. } else {
  241. showError(message);
  242. }
  243. };
  244. // 划转邀请额度
  245. const transfer = async () => {
  246. if (transferAmount < getQuotaPerUnit()) {
  247. showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
  248. return;
  249. }
  250. const res = await API.post(`/api/user/aff_transfer`, {
  251. quota: transferAmount,
  252. });
  253. const { success, message } = res.data;
  254. if (success) {
  255. showSuccess(message);
  256. setOpenTransfer(false);
  257. getUserQuota().then();
  258. } else {
  259. showError(message);
  260. }
  261. };
  262. // 复制邀请链接
  263. const handleAffLinkClick = async () => {
  264. await copy(affLink);
  265. showSuccess(t('邀请链接已复制到剪切板'));
  266. };
  267. useEffect(() => {
  268. if (userState?.user?.id) {
  269. setUserDataLoading(false);
  270. setUserQuota(userState.user.quota);
  271. } else {
  272. getUserQuota().then();
  273. }
  274. getAffLink().then();
  275. setTransferAmount(getQuotaPerUnit());
  276. let payMethods = localStorage.getItem('pay_methods');
  277. try {
  278. payMethods = JSON.parse(payMethods);
  279. if (payMethods && payMethods.length > 0) {
  280. // 检查name和type是否为空
  281. payMethods = payMethods.filter((method) => {
  282. return method.name && method.type;
  283. });
  284. // 如果没有color,则设置默认颜色
  285. payMethods = payMethods.map((method) => {
  286. if (!method.color) {
  287. if (method.type === 'zfb') {
  288. method.color = 'rgba(var(--semi-blue-5), 1)';
  289. } else if (method.type === 'wx') {
  290. method.color = 'rgba(var(--semi-green-5), 1)';
  291. } else {
  292. method.color = 'rgba(var(--semi-primary-5), 1)';
  293. }
  294. }
  295. return method;
  296. });
  297. setPayMethods(payMethods);
  298. }
  299. } catch (e) {
  300. console.log(e);
  301. showError(t('支付方式配置错误, 请联系管理员'));
  302. }
  303. }, []);
  304. useEffect(() => {
  305. if (statusState?.status) {
  306. setMinTopUp(statusState.status.min_topup || 1);
  307. setTopUpCount(statusState.status.min_topup || 1);
  308. setTopUpLink(statusState.status.top_up_link || '');
  309. setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
  310. setPriceRatio(statusState.status.price || 1);
  311. }
  312. }, [statusState?.status]);
  313. const renderAmount = () => {
  314. return amount + ' ' + t('元');
  315. };
  316. const getAmount = async (value) => {
  317. if (value === undefined) {
  318. value = topUpCount;
  319. }
  320. setAmountLoading(true);
  321. try {
  322. const res = await API.post('/api/user/amount', {
  323. amount: parseFloat(value),
  324. top_up_code: topUpCode,
  325. });
  326. if (res !== undefined) {
  327. const { message, data } = res.data;
  328. if (message === 'success') {
  329. setAmount(parseFloat(data));
  330. } else {
  331. setAmount(0);
  332. Toast.error({ content: '错误:' + data, id: 'getAmount' });
  333. }
  334. } else {
  335. showError(res);
  336. }
  337. } catch (err) {
  338. console.log(err);
  339. }
  340. setAmountLoading(false);
  341. };
  342. const handleCancel = () => {
  343. setOpen(false);
  344. };
  345. const handleTransferCancel = () => {
  346. setOpenTransfer(false);
  347. };
  348. // 选择预设充值额度
  349. const selectPresetAmount = (preset) => {
  350. setTopUpCount(preset.value);
  351. setSelectedPreset(preset.value);
  352. setAmount(preset.value * priceRatio);
  353. };
  354. // 格式化大数字显示
  355. const formatLargeNumber = (num) => {
  356. return num.toString();
  357. };
  358. return (
  359. <div className='mx-auto relative min-h-screen lg:min-h-0'>
  360. {/* 划转模态框 */}
  361. <Modal
  362. title={
  363. <div className='flex items-center'>
  364. <CreditCard className='mr-2' size={18} />
  365. {t('划转邀请额度')}
  366. </div>
  367. }
  368. visible={openTransfer}
  369. onOk={transfer}
  370. onCancel={handleTransferCancel}
  371. maskClosable={false}
  372. size='small'
  373. centered
  374. >
  375. <div className='space-y-4'>
  376. <div>
  377. <Typography.Text strong className='block mb-2'>
  378. {t('可用邀请额度')}
  379. </Typography.Text>
  380. <Input
  381. value={renderQuota(userState?.user?.aff_quota)}
  382. disabled
  383. size='large'
  384. />
  385. </div>
  386. <div>
  387. <Typography.Text strong className='block mb-2'>
  388. {t('划转额度')} ({t('最低') + renderQuota(getQuotaPerUnit())})
  389. </Typography.Text>
  390. <InputNumber
  391. min={getQuotaPerUnit()}
  392. max={userState?.user?.aff_quota || 0}
  393. value={transferAmount}
  394. onChange={(value) => setTransferAmount(value)}
  395. size='large'
  396. className='w-full'
  397. />
  398. </div>
  399. </div>
  400. </Modal>
  401. {/* 充值确认模态框 */}
  402. <Modal
  403. title={
  404. <div className='flex items-center'>
  405. <CreditCard className='mr-2' size={18} />
  406. {t('充值确认')}
  407. </div>
  408. }
  409. visible={open}
  410. onOk={onlineTopUp}
  411. onCancel={handleCancel}
  412. maskClosable={false}
  413. size='small'
  414. centered
  415. confirmLoading={confirmLoading}
  416. >
  417. <div className='space-y-4'>
  418. <div className='flex justify-between items-center py-2'>
  419. <Text strong>{t('充值数量')}:</Text>
  420. <Text>{renderQuotaWithAmount(topUpCount)}</Text>
  421. </div>
  422. <div className='flex justify-between items-center py-2'>
  423. <Text strong>{t('实付金额')}:</Text>
  424. {amountLoading ? (
  425. <Skeleton.Title style={{ width: '60px', height: '16px' }} />
  426. ) : (
  427. <Text type='danger' strong>
  428. {renderAmount()}
  429. </Text>
  430. )}
  431. </div>
  432. <div className='flex justify-between items-center py-2'>
  433. <Text strong>{t('支付方式')}:</Text>
  434. <Text>
  435. {(() => {
  436. const payMethod = payMethods.find(
  437. (method) => method.type === payWay,
  438. );
  439. if (payMethod) {
  440. return (
  441. <div className='flex items-center'>
  442. {payMethod.type === 'zfb' ? (
  443. <SiAlipay className='mr-1' size={16} />
  444. ) : payMethod.type === 'wx' ? (
  445. <SiWechat className='mr-1' size={16} />
  446. ) : (
  447. <CreditCard className='mr-1' size={16} />
  448. )}
  449. {payMethod.name}
  450. </div>
  451. );
  452. } else {
  453. // 默认充值方式
  454. return payWay === 'zfb' ? (
  455. <div className='flex items-center'>
  456. <SiAlipay className='mr-1' size={16} />
  457. {t('支付宝')}
  458. </div>
  459. ) : (
  460. <div className='flex items-center'>
  461. <SiWechat className='mr-1' size={16} />
  462. {t('微信')}
  463. </div>
  464. );
  465. }
  466. })()}
  467. </Text>
  468. </div>
  469. </div>
  470. </Modal>
  471. <div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
  472. {/* 左侧充值区域 */}
  473. <div className='lg:col-span-7 space-y-6 w-full'>
  474. {/* 在线充值卡片 */}
  475. <Card
  476. className='!rounded-2xl'
  477. shadows='always'
  478. bordered={false}
  479. header={
  480. <div className='px-5 py-4 pb-0'>
  481. <div className='flex items-center justify-between'>
  482. <div className='flex items-center'>
  483. <Avatar
  484. className='mr-3 shadow-md flex-shrink-0'
  485. color='blue'
  486. >
  487. <CreditCard size={24} />
  488. </Avatar>
  489. <div>
  490. <Title heading={5} style={{ margin: 0 }}>
  491. {t('在线充值')}
  492. </Title>
  493. <Text type='tertiary' className='text-sm'>
  494. {t('快速方便的充值方式')}
  495. </Text>
  496. </div>
  497. </div>
  498. <div className='flex items-center'>
  499. {userDataLoading ? (
  500. <Skeleton.Paragraph style={{ width: '120px' }} rows={1} />
  501. ) : (
  502. <Text type='tertiary' className='hidden sm:block'>
  503. <div className='flex items-center'>
  504. <User size={14} className='mr-1' />
  505. <span className='hidden md:inline'>
  506. {getUsername()} ({getUserRole()})
  507. </span>
  508. <span className='md:hidden'>{getUsername()}</span>
  509. </div>
  510. </Text>
  511. )}
  512. </div>
  513. </div>
  514. </div>
  515. }
  516. >
  517. <div className='space-y-4'>
  518. {/* 账户余额信息 */}
  519. <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-2'>
  520. <Card className='!rounded-2xl'>
  521. <Text type='tertiary' className='mb-1'>
  522. {t('当前余额')}
  523. </Text>
  524. {userDataLoading ? (
  525. <Skeleton.Title
  526. style={{ width: '100px', height: '30px' }}
  527. />
  528. ) : (
  529. <div className='text-xl font-semibold mt-2'>
  530. {renderQuota(userState?.user?.quota || userQuota)}
  531. </div>
  532. )}
  533. </Card>
  534. <Card className='!rounded-2xl'>
  535. <Text type='tertiary' className='mb-1'>
  536. {t('历史消耗')}
  537. </Text>
  538. {userDataLoading ? (
  539. <Skeleton.Title
  540. style={{ width: '100px', height: '30px' }}
  541. />
  542. ) : (
  543. <div className='text-xl font-semibold mt-2'>
  544. {renderQuota(userState?.user?.used_quota || 0)}
  545. </div>
  546. )}
  547. </Card>
  548. </div>
  549. {enableOnlineTopUp && (
  550. <>
  551. {/* 预设充值额度卡片网格 */}
  552. <div>
  553. <Text strong className='block mb-3'>
  554. {t('选择充值额度')}
  555. </Text>
  556. <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3'>
  557. {presetAmounts.map((preset, index) => (
  558. <Card
  559. key={index}
  560. onClick={() => selectPresetAmount(preset)}
  561. className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${selectedPreset === preset.value
  562. ? 'border-blue-500'
  563. : 'border-gray-200 hover:border-gray-300'
  564. }`}
  565. bodyStyle={{ textAlign: 'center' }}
  566. >
  567. <div className='font-medium text-lg flex items-center justify-center mb-1'>
  568. <Coins size={16} className='mr-0.5' />
  569. {formatLargeNumber(preset.value)}
  570. </div>
  571. <div className='text-xs text-gray-500'>
  572. {t('实付')} ¥
  573. {(preset.value * priceRatio).toFixed(2)}
  574. </div>
  575. </Card>
  576. ))}
  577. </div>
  578. </div>
  579. {/* 桌面端显示的自定义金额和支付按钮 */}
  580. <div className='hidden md:block space-y-4'>
  581. <Divider style={{ margin: '24px 0' }}>
  582. <Text className='text-sm font-medium'>
  583. {t('或输入自定义金额')}
  584. </Text>
  585. </Divider>
  586. <div>
  587. <div className='flex justify-between mb-2'>
  588. <Text strong>{t('充值数量')}</Text>
  589. {amountLoading ? (
  590. <Skeleton.Title
  591. style={{ width: '80px', height: '16px' }}
  592. />
  593. ) : (
  594. <Text type='tertiary'>
  595. {t('实付金额:') + renderAmount()}
  596. </Text>
  597. )}
  598. </div>
  599. <InputNumber
  600. disabled={!enableOnlineTopUp}
  601. placeholder={
  602. t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
  603. }
  604. value={topUpCount}
  605. min={minTopUp}
  606. max={999999999}
  607. step={1}
  608. precision={0}
  609. onChange={async (value) => {
  610. if (value && value >= 1) {
  611. setTopUpCount(value);
  612. setSelectedPreset(null);
  613. await getAmount(value);
  614. }
  615. }}
  616. onBlur={(e) => {
  617. const value = parseInt(e.target.value);
  618. if (!value || value < 1) {
  619. setTopUpCount(1);
  620. getAmount(1);
  621. }
  622. }}
  623. size='large'
  624. className='w-full'
  625. formatter={(value) => (value ? `${value}` : '')}
  626. parser={(value) =>
  627. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  628. }
  629. />
  630. </div>
  631. <div>
  632. <Text strong className='block mb-3'>
  633. {t('选择支付方式')}
  634. </Text>
  635. {payMethods.length === 2 ? (
  636. <div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
  637. {payMethods.map((payMethod) => (
  638. <Button
  639. key={payMethod.type}
  640. type='primary'
  641. onClick={() => preTopUp(payMethod.type)}
  642. size='large'
  643. disabled={!enableOnlineTopUp}
  644. loading={paymentLoading && payWay === payMethod.type}
  645. icon={
  646. payMethod.type === 'zfb' ? (
  647. <SiAlipay size={16} />
  648. ) : payMethod.type === 'wx' ? (
  649. <SiWechat size={16} />
  650. ) : (
  651. <CreditCard size={16} />
  652. )
  653. }
  654. style={{
  655. height: '40px',
  656. color: payMethod.color,
  657. }}
  658. className='transition-all hover:shadow-md w-full'
  659. >
  660. <span className='ml-1'>{payMethod.name}</span>
  661. </Button>
  662. ))}
  663. </div>
  664. ) : payMethods.length === 3 ? (
  665. <div className='grid grid-cols-1 sm:grid-cols-3 gap-3'>
  666. {payMethods.map((payMethod) => (
  667. <Button
  668. key={payMethod.type}
  669. type='primary'
  670. onClick={() => preTopUp(payMethod.type)}
  671. size='large'
  672. disabled={!enableOnlineTopUp}
  673. loading={paymentLoading && payWay === payMethod.type}
  674. icon={
  675. payMethod.type === 'zfb' ? (
  676. <SiAlipay size={16} />
  677. ) : payMethod.type === 'wx' ? (
  678. <SiWechat size={16} />
  679. ) : (
  680. <CreditCard size={16} />
  681. )
  682. }
  683. style={{
  684. height: '40px',
  685. color: payMethod.color,
  686. }}
  687. className='transition-all hover:shadow-md w-full'
  688. >
  689. <span className='ml-1'>{payMethod.name}</span>
  690. </Button>
  691. ))}
  692. </div>
  693. ) : payMethods.length > 3 ? (
  694. <div className='grid grid-cols-2 sm:grid-cols-4 gap-3'>
  695. {payMethods.map((payMethod) => (
  696. <Card
  697. key={payMethod.type}
  698. onClick={() => preTopUp(payMethod.type)}
  699. disabled={!enableOnlineTopUp}
  700. className={`cursor-pointer !rounded-xl p-0 transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type
  701. ? 'border-blue-400'
  702. : 'border-gray-200 hover:border-gray-300'
  703. }`}
  704. bodyStyle={{
  705. padding: '10px',
  706. textAlign: 'center',
  707. opacity: !enableOnlineTopUp ? 0.5 : 1
  708. }}
  709. >
  710. {paymentLoading && payWay === payMethod.type ? (
  711. <div className='flex flex-col items-center justify-center h-full'>
  712. <div className='mb-1'>
  713. <div className='animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500'></div>
  714. </div>
  715. <div className='text-xs text-gray-500'>{t('处理中')}</div>
  716. </div>
  717. ) : (
  718. <>
  719. <div className='flex items-center justify-center mb-1'>
  720. {payMethod.type === 'zfb' ? (
  721. <SiAlipay size={20} color={payMethod.color} />
  722. ) : payMethod.type === 'wx' ? (
  723. <SiWechat size={20} color={payMethod.color} />
  724. ) : (
  725. <CreditCard size={20} color={payMethod.color} />
  726. )}
  727. </div>
  728. <div className='text-sm font-medium'>{payMethod.name}</div>
  729. </>
  730. )}
  731. </Card>
  732. ))}
  733. </div>
  734. ) : (
  735. <div className='grid grid-cols-1 gap-3'>
  736. {payMethods.map((payMethod) => (
  737. <Button
  738. key={payMethod.type}
  739. type='primary'
  740. onClick={() => preTopUp(payMethod.type)}
  741. size='large'
  742. disabled={!enableOnlineTopUp}
  743. loading={paymentLoading && payWay === payMethod.type}
  744. icon={
  745. payMethod.type === 'zfb' ? (
  746. <SiAlipay size={16} />
  747. ) : payMethod.type === 'wx' ? (
  748. <SiWechat size={16} />
  749. ) : (
  750. <CreditCard size={16} />
  751. )
  752. }
  753. style={{
  754. height: '40px',
  755. color: payMethod.color,
  756. }}
  757. className='transition-all hover:shadow-md w-full'
  758. >
  759. <span className='ml-1'>{payMethod.name}</span>
  760. </Button>
  761. ))}
  762. </div>
  763. )}
  764. </div>
  765. </div>
  766. </>
  767. )}
  768. {!enableOnlineTopUp && (
  769. <Banner
  770. type='warning'
  771. description={t(
  772. '管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。',
  773. )}
  774. closeIcon={null}
  775. className='!rounded-2xl'
  776. />
  777. )}
  778. <Divider style={{ margin: '24px 0' }}>
  779. <Text className='text-sm font-medium'>{t('兑换码充值')}</Text>
  780. </Divider>
  781. <Card className='!rounded-2xl'>
  782. <div className='flex items-start mb-4'>
  783. <Gift size={16} className='mr-2 mt-0.5' />
  784. <Text strong>{t('使用兑换码快速充值')}</Text>
  785. </div>
  786. <div className='mb-4'>
  787. <Input
  788. placeholder={t('请输入兑换码')}
  789. value={redemptionCode}
  790. onChange={(value) => setRedemptionCode(value)}
  791. size='large'
  792. />
  793. </div>
  794. <div className='flex flex-col sm:flex-row gap-3'>
  795. {topUpLink && (
  796. <Button
  797. type='secondary'
  798. onClick={openTopUpLink}
  799. size='large'
  800. className='flex-1'
  801. icon={<LinkIcon size={16} />}
  802. style={{ height: '40px' }}
  803. >
  804. {t('获取兑换码')}
  805. </Button>
  806. )}
  807. <Button
  808. type='primary'
  809. onClick={topUp}
  810. disabled={isSubmitting || !redemptionCode}
  811. loading={isSubmitting}
  812. size='large'
  813. className='flex-1'
  814. style={{ height: '40px' }}
  815. >
  816. {isSubmitting ? t('兑换中...') : t('兑换')}
  817. </Button>
  818. </div>
  819. </Card>
  820. </div>
  821. </Card>
  822. </div>
  823. {/* 右侧邀请信息卡片 */}
  824. <div className='lg:col-span-5'>
  825. <Card
  826. className='!rounded-2xl'
  827. shadows='always'
  828. bordered={false}
  829. header={
  830. <div className='px-5 py-4 pb-0'>
  831. <div className='flex items-center justify-between'>
  832. <div className='flex items-center'>
  833. <Avatar
  834. className='mr-3 shadow-md flex-shrink-0'
  835. color='green'
  836. >
  837. <Users size={24} />
  838. </Avatar>
  839. <div>
  840. <Title heading={5} style={{ margin: 0 }}>
  841. {t('邀请奖励')}
  842. </Title>
  843. <Text type='tertiary' className='text-sm'>
  844. {t('邀请好友获得额外奖励')}
  845. </Text>
  846. </div>
  847. </div>
  848. </div>
  849. </div>
  850. }
  851. >
  852. <div className='space-y-6'>
  853. <div className='grid grid-cols-1 gap-4'>
  854. <Card className='!rounded-2xl'>
  855. <div className='flex justify-between items-center'>
  856. <Text type='tertiary'>{t('待使用收益')}</Text>
  857. <Button
  858. type='primary'
  859. theme='solid'
  860. size='small'
  861. disabled={
  862. !userState?.user?.aff_quota ||
  863. userState?.user?.aff_quota <= 0
  864. }
  865. onClick={() => setOpenTransfer(true)}
  866. >
  867. {t('划转到余额')}
  868. </Button>
  869. </div>
  870. <div className='text-2xl font-semibold mt-2'>
  871. {renderQuota(userState?.user?.aff_quota || 0)}
  872. </div>
  873. </Card>
  874. <div className='grid grid-cols-2 gap-4'>
  875. <Card className='!rounded-2xl'>
  876. <Text type='tertiary'>{t('总收益')}</Text>
  877. <div className='text-xl font-semibold mt-2'>
  878. {renderQuota(userState?.user?.aff_history_quota || 0)}
  879. </div>
  880. </Card>
  881. <Card className='!rounded-2xl'>
  882. <Text type='tertiary'>{t('邀请人数')}</Text>
  883. <div className='text-xl font-semibold mt-2 flex items-center'>
  884. <Users size={16} className='mr-1' />
  885. {userState?.user?.aff_count || 0}
  886. </div>
  887. </Card>
  888. </div>
  889. </div>
  890. <div className='space-y-4'>
  891. <Title heading={6}>{t('邀请链接')}</Title>
  892. <Input
  893. value={affLink}
  894. readOnly
  895. size='large'
  896. suffix={
  897. <Button
  898. type='primary'
  899. theme='light'
  900. onClick={handleAffLinkClick}
  901. icon={<Copy size={14} />}
  902. >
  903. {t('复制')}
  904. </Button>
  905. }
  906. />
  907. <div className='mt-4'>
  908. <Card className='!rounded-2xl'>
  909. <div className='space-y-4'>
  910. <div className='flex items-start'>
  911. <div className='w-1.5 h-1.5 rounded-full bg-blue-500 mt-2 mr-3 flex-shrink-0'></div>
  912. <Text type='tertiary' className='text-sm leading-6'>
  913. {t('邀请好友注册,好友充值后您可获得相应奖励')}
  914. </Text>
  915. </div>
  916. <div className='flex items-start'>
  917. <div className='w-1.5 h-1.5 rounded-full bg-green-500 mt-2 mr-3 flex-shrink-0'></div>
  918. <Text type='tertiary' className='text-sm leading-6'>
  919. {t('通过划转功能将奖励额度转入到您的账户余额中')}
  920. </Text>
  921. </div>
  922. <div className='flex items-start'>
  923. <div className='w-1.5 h-1.5 rounded-full bg-purple-500 mt-2 mr-3 flex-shrink-0'></div>
  924. <Text type='tertiary' className='text-sm leading-6'>
  925. {t('邀请的好友越多,获得的奖励越多')}
  926. </Text>
  927. </div>
  928. </div>
  929. </Card>
  930. </div>
  931. </div>
  932. </div>
  933. </Card>
  934. </div>
  935. </div>
  936. {/* 移动端底部固定的自定义金额和支付区域 */}
  937. {enableOnlineTopUp && (
  938. <div
  939. className='md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50'
  940. style={{ background: 'var(--semi-color-bg-0)' }}
  941. >
  942. <div className='space-y-4'>
  943. <div>
  944. <div className='flex justify-between mb-2'>
  945. <Text strong>{t('充值数量')}</Text>
  946. {amountLoading ? (
  947. <Skeleton.Title style={{ width: '80px', height: '16px' }} />
  948. ) : (
  949. <Text type='tertiary'>
  950. {t('实付金额:') + renderAmount()}
  951. </Text>
  952. )}
  953. </div>
  954. <InputNumber
  955. disabled={!enableOnlineTopUp}
  956. placeholder={
  957. t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
  958. }
  959. value={topUpCount}
  960. min={minTopUp}
  961. max={999999999}
  962. step={1}
  963. precision={0}
  964. onChange={async (value) => {
  965. if (value && value >= 1) {
  966. setTopUpCount(value);
  967. setSelectedPreset(null);
  968. await getAmount(value);
  969. }
  970. }}
  971. onBlur={(e) => {
  972. const value = parseInt(e.target.value);
  973. if (!value || value < 1) {
  974. setTopUpCount(1);
  975. getAmount(1);
  976. }
  977. }}
  978. className='w-full'
  979. formatter={(value) => (value ? `${value}` : '')}
  980. parser={(value) =>
  981. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  982. }
  983. />
  984. </div>
  985. <div>
  986. {payMethods.length === 2 ? (
  987. <div className='grid grid-cols-2 gap-3'>
  988. {payMethods.map((payMethod) => (
  989. <Button
  990. key={payMethod.type}
  991. type='primary'
  992. onClick={() => preTopUp(payMethod.type)}
  993. disabled={!enableOnlineTopUp}
  994. loading={paymentLoading && payWay === payMethod.type}
  995. icon={
  996. payMethod.type === 'zfb' ? (
  997. <SiAlipay size={16} />
  998. ) : payMethod.type === 'wx' ? (
  999. <SiWechat size={16} />
  1000. ) : (
  1001. <CreditCard size={16} />
  1002. )
  1003. }
  1004. style={{
  1005. color: payMethod.color,
  1006. }}
  1007. className='h-10'
  1008. >
  1009. <span className='ml-1'>{payMethod.name}</span>
  1010. </Button>
  1011. ))}
  1012. </div>
  1013. ) : (
  1014. <div className='grid grid-cols-4 gap-2'>
  1015. {payMethods.map((payMethod) => (
  1016. <Card
  1017. key={payMethod.type}
  1018. onClick={() => preTopUp(payMethod.type)}
  1019. disabled={!enableOnlineTopUp}
  1020. className={`cursor-pointer !rounded-xl p-0 transition-all ${paymentLoading && payWay === payMethod.type
  1021. ? 'border-blue-400'
  1022. : 'border-gray-200'
  1023. }`}
  1024. bodyStyle={{
  1025. padding: '8px',
  1026. textAlign: 'center',
  1027. opacity: !enableOnlineTopUp ? 0.5 : 1
  1028. }}
  1029. >
  1030. {paymentLoading && payWay === payMethod.type ? (
  1031. <div className='animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500 mx-auto'></div>
  1032. ) : (
  1033. <>
  1034. <div className='flex justify-center'>
  1035. {payMethod.type === 'zfb' ? (
  1036. <SiAlipay size={18} color={payMethod.color} />
  1037. ) : payMethod.type === 'wx' ? (
  1038. <SiWechat size={18} color={payMethod.color} />
  1039. ) : (
  1040. <CreditCard size={18} color={payMethod.color} />
  1041. )}
  1042. </div>
  1043. <div className='text-xs mt-1'>{payMethod.name}</div>
  1044. </>
  1045. )}
  1046. </Card>
  1047. ))}
  1048. </div>
  1049. )}
  1050. </div>
  1051. </div>
  1052. </div>
  1053. )}
  1054. </div>
  1055. );
  1056. };
  1057. export default TopUp;