index.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  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 ${
  562. selectedPreset === preset.value
  563. ? 'border-blue-500'
  564. : 'border-gray-200 hover:border-gray-300'
  565. }`}
  566. bodyStyle={{ textAlign: 'center' }}
  567. >
  568. <div className='font-medium text-lg flex items-center justify-center mb-1'>
  569. <Coins size={16} className='mr-0.5' />
  570. {formatLargeNumber(preset.value)}
  571. </div>
  572. <div className='text-xs text-gray-500'>
  573. {t('实付')} ¥
  574. {(preset.value * priceRatio).toFixed(2)}
  575. </div>
  576. </Card>
  577. ))}
  578. </div>
  579. </div>
  580. {/* 桌面端显示的自定义金额和支付按钮 */}
  581. <div className='hidden md:block space-y-4'>
  582. <Divider style={{ margin: '24px 0' }}>
  583. <Text className='text-sm font-medium'>
  584. {t('或输入自定义金额')}
  585. </Text>
  586. </Divider>
  587. <div>
  588. <div className='flex justify-between mb-2'>
  589. <Text strong>{t('充值数量')}</Text>
  590. {amountLoading ? (
  591. <Skeleton.Title
  592. style={{ width: '80px', height: '16px' }}
  593. />
  594. ) : (
  595. <Text type='tertiary'>
  596. {t('实付金额:') + renderAmount()}
  597. </Text>
  598. )}
  599. </div>
  600. <InputNumber
  601. disabled={!enableOnlineTopUp}
  602. placeholder={
  603. t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
  604. }
  605. value={topUpCount}
  606. min={minTopUp}
  607. max={999999999}
  608. step={1}
  609. precision={0}
  610. onChange={async (value) => {
  611. if (value && value >= 1) {
  612. setTopUpCount(value);
  613. setSelectedPreset(null);
  614. await getAmount(value);
  615. }
  616. }}
  617. onBlur={(e) => {
  618. const value = parseInt(e.target.value);
  619. if (!value || value < 1) {
  620. setTopUpCount(1);
  621. getAmount(1);
  622. }
  623. }}
  624. size='large'
  625. className='w-full'
  626. formatter={(value) => (value ? `${value}` : '')}
  627. parser={(value) =>
  628. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  629. }
  630. />
  631. </div>
  632. <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
  633. {/* <Button
  634. type='primary'
  635. onClick={() => preTopUp('zfb')}
  636. size='large'
  637. disabled={!enableOnlineTopUp}
  638. loading={paymentLoading && payWay === 'zfb'}
  639. icon={<SiAlipay size={18} />}
  640. style={{ height: '44px' }}
  641. >
  642. <span className='ml-2'>{t('支付宝')}</span>
  643. </Button>
  644. <Button
  645. type='primary'
  646. onClick={() => preTopUp('wx')}
  647. size='large'
  648. disabled={!enableOnlineTopUp}
  649. loading={paymentLoading && payWay === 'wx'}
  650. icon={<SiWechat size={18} />}
  651. style={{ height: '44px' }}
  652. >
  653. <span className='ml-2'>{t('微信')}</span>
  654. </Button> */}
  655. {payMethods.map((payMethod) => (
  656. <Button
  657. key={payMethod.type}
  658. type='primary'
  659. onClick={() => preTopUp(payMethod.type)}
  660. size='large'
  661. disabled={!enableOnlineTopUp}
  662. loading={paymentLoading && payWay === payMethod.type}
  663. icon={
  664. payMethod.type === 'zfb' ? (
  665. <SiAlipay size={18} />
  666. ) : payMethod.type === 'wx' ? (
  667. <SiWechat size={18} />
  668. ) : (
  669. <CreditCard size={18} />
  670. )
  671. }
  672. style={{
  673. height: '44px',
  674. color: payMethod.color,
  675. }}
  676. >
  677. <span className='ml-2'>{payMethod.name}</span>
  678. </Button>
  679. ))}
  680. </div>
  681. </div>
  682. </>
  683. )}
  684. {!enableOnlineTopUp && (
  685. <Banner
  686. type='warning'
  687. description={t(
  688. '管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。',
  689. )}
  690. closeIcon={null}
  691. className='!rounded-2xl'
  692. />
  693. )}
  694. <Divider style={{ margin: '24px 0' }}>
  695. <Text className='text-sm font-medium'>{t('兑换码充值')}</Text>
  696. </Divider>
  697. <Card className='!rounded-2xl'>
  698. <div className='flex items-start mb-4'>
  699. <Gift size={16} className='mr-2 mt-0.5' />
  700. <Text strong>{t('使用兑换码快速充值')}</Text>
  701. </div>
  702. <div className='mb-4'>
  703. <Input
  704. placeholder={t('请输入兑换码')}
  705. value={redemptionCode}
  706. onChange={(value) => setRedemptionCode(value)}
  707. size='large'
  708. />
  709. </div>
  710. <div className='flex flex-col sm:flex-row gap-3'>
  711. {topUpLink && (
  712. <Button
  713. type='secondary'
  714. onClick={openTopUpLink}
  715. size='large'
  716. className='flex-1'
  717. icon={<LinkIcon size={16} />}
  718. style={{ height: '40px' }}
  719. >
  720. {t('获取兑换码')}
  721. </Button>
  722. )}
  723. <Button
  724. type='primary'
  725. onClick={topUp}
  726. disabled={isSubmitting || !redemptionCode}
  727. loading={isSubmitting}
  728. size='large'
  729. className='flex-1'
  730. style={{ height: '40px' }}
  731. >
  732. {isSubmitting ? t('兑换中...') : t('兑换')}
  733. </Button>
  734. </div>
  735. </Card>
  736. </div>
  737. </Card>
  738. </div>
  739. {/* 右侧邀请信息卡片 */}
  740. <div className='lg:col-span-5'>
  741. <Card
  742. className='!rounded-2xl'
  743. shadows='always'
  744. bordered={false}
  745. header={
  746. <div className='px-5 py-4 pb-0'>
  747. <div className='flex items-center justify-between'>
  748. <div className='flex items-center'>
  749. <Avatar
  750. className='mr-3 shadow-md flex-shrink-0'
  751. color='green'
  752. >
  753. <Users size={24} />
  754. </Avatar>
  755. <div>
  756. <Title heading={5} style={{ margin: 0 }}>
  757. {t('邀请奖励')}
  758. </Title>
  759. <Text type='tertiary' className='text-sm'>
  760. {t('邀请好友获得额外奖励')}
  761. </Text>
  762. </div>
  763. </div>
  764. </div>
  765. </div>
  766. }
  767. >
  768. <div className='space-y-6'>
  769. <div className='grid grid-cols-1 gap-4'>
  770. <Card className='!rounded-2xl'>
  771. <div className='flex justify-between items-center'>
  772. <Text type='tertiary'>{t('待使用收益')}</Text>
  773. <Button
  774. type='primary'
  775. theme='solid'
  776. size='small'
  777. disabled={
  778. !userState?.user?.aff_quota ||
  779. userState?.user?.aff_quota <= 0
  780. }
  781. onClick={() => setOpenTransfer(true)}
  782. >
  783. {t('划转到余额')}
  784. </Button>
  785. </div>
  786. <div className='text-2xl font-semibold mt-2'>
  787. {renderQuota(userState?.user?.aff_quota || 0)}
  788. </div>
  789. </Card>
  790. <div className='grid grid-cols-2 gap-4'>
  791. <Card className='!rounded-2xl'>
  792. <Text type='tertiary'>{t('总收益')}</Text>
  793. <div className='text-xl font-semibold mt-2'>
  794. {renderQuota(userState?.user?.aff_history_quota || 0)}
  795. </div>
  796. </Card>
  797. <Card className='!rounded-2xl'>
  798. <Text type='tertiary'>{t('邀请人数')}</Text>
  799. <div className='text-xl font-semibold mt-2 flex items-center'>
  800. <Users size={16} className='mr-1' />
  801. {userState?.user?.aff_count || 0}
  802. </div>
  803. </Card>
  804. </div>
  805. </div>
  806. <div className='space-y-4'>
  807. <Title heading={6}>{t('邀请链接')}</Title>
  808. <Input
  809. value={affLink}
  810. readOnly
  811. size='large'
  812. suffix={
  813. <Button
  814. type='primary'
  815. theme='light'
  816. onClick={handleAffLinkClick}
  817. icon={<Copy size={14} />}
  818. >
  819. {t('复制')}
  820. </Button>
  821. }
  822. />
  823. <div className='mt-4'>
  824. <Card className='!rounded-2xl'>
  825. <div className='space-y-4'>
  826. <div className='flex items-start'>
  827. <div className='w-1.5 h-1.5 rounded-full bg-blue-500 mt-2 mr-3 flex-shrink-0'></div>
  828. <Text type='tertiary' className='text-sm leading-6'>
  829. {t('邀请好友注册,好友充值后您可获得相应奖励')}
  830. </Text>
  831. </div>
  832. <div className='flex items-start'>
  833. <div className='w-1.5 h-1.5 rounded-full bg-green-500 mt-2 mr-3 flex-shrink-0'></div>
  834. <Text type='tertiary' className='text-sm leading-6'>
  835. {t('通过划转功能将奖励额度转入到您的账户余额中')}
  836. </Text>
  837. </div>
  838. <div className='flex items-start'>
  839. <div className='w-1.5 h-1.5 rounded-full bg-purple-500 mt-2 mr-3 flex-shrink-0'></div>
  840. <Text type='tertiary' className='text-sm leading-6'>
  841. {t('邀请的好友越多,获得的奖励越多')}
  842. </Text>
  843. </div>
  844. </div>
  845. </Card>
  846. </div>
  847. </div>
  848. </div>
  849. </Card>
  850. </div>
  851. </div>
  852. {/* 移动端底部固定的自定义金额和支付区域 */}
  853. {enableOnlineTopUp && (
  854. <div
  855. className='md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50'
  856. style={{ background: 'var(--semi-color-bg-0)' }}
  857. >
  858. <div className='space-y-4'>
  859. <div>
  860. <div className='flex justify-between mb-2'>
  861. <Text strong>{t('充值数量')}</Text>
  862. {amountLoading ? (
  863. <Skeleton.Title style={{ width: '80px', height: '16px' }} />
  864. ) : (
  865. <Text type='tertiary'>
  866. {t('实付金额:') + renderAmount()}
  867. </Text>
  868. )}
  869. </div>
  870. <InputNumber
  871. disabled={!enableOnlineTopUp}
  872. placeholder={
  873. t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
  874. }
  875. value={topUpCount}
  876. min={minTopUp}
  877. max={999999999}
  878. step={1}
  879. precision={0}
  880. onChange={async (value) => {
  881. if (value && value >= 1) {
  882. setTopUpCount(value);
  883. setSelectedPreset(null);
  884. await getAmount(value);
  885. }
  886. }}
  887. onBlur={(e) => {
  888. const value = parseInt(e.target.value);
  889. if (!value || value < 1) {
  890. setTopUpCount(1);
  891. getAmount(1);
  892. }
  893. }}
  894. className='w-full'
  895. formatter={(value) => (value ? `${value}` : '')}
  896. parser={(value) =>
  897. value ? parseInt(value.replace(/[^\d]/g, '')) : 0
  898. }
  899. />
  900. </div>
  901. <div className='grid grid-cols-2 gap-4'>
  902. {/* <Button
  903. type='primary'
  904. onClick={() => preTopUp('zfb')}
  905. disabled={!enableOnlineTopUp}
  906. loading={paymentLoading && payWay === 'zfb'}
  907. icon={<SiAlipay size={18} />}
  908. >
  909. <span className='ml-2'>{t('支付宝')}</span>
  910. </Button>
  911. <Button
  912. type='primary'
  913. onClick={() => preTopUp('wx')}
  914. disabled={!enableOnlineTopUp}
  915. loading={paymentLoading && payWay === 'wx'}
  916. icon={<SiWechat size={18} />}
  917. >
  918. <span className='ml-2'>{t('微信')}</span>
  919. </Button> */}
  920. {payMethods.map((payMethod) => (
  921. <Button
  922. key={payMethod.type}
  923. type='primary'
  924. onClick={() => preTopUp(payMethod.type)}
  925. disabled={!enableOnlineTopUp}
  926. loading={paymentLoading && payWay === payMethod.type}
  927. icon={
  928. payMethod.type === 'zfb' ? (
  929. <SiAlipay size={18} />
  930. ) : payMethod.type === 'wx' ? (
  931. <SiWechat size={18} />
  932. ) : (
  933. <CreditCard size={18} />
  934. )
  935. }
  936. style={{
  937. color: payMethod.color,
  938. }}
  939. >
  940. <span className='ml-2'>{payMethod.name}</span>
  941. </Button>
  942. ))}
  943. </div>
  944. </div>
  945. </div>
  946. )}
  947. </div>
  948. );
  949. };
  950. export default TopUp;