RechargeCard.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact [email protected]
  14. */
  15. import React from 'react';
  16. import {
  17. Avatar,
  18. Typography,
  19. Card,
  20. Button,
  21. Input,
  22. InputNumber,
  23. Banner,
  24. Skeleton,
  25. Divider,
  26. Tabs,
  27. TabPane,
  28. } from '@douyinfe/semi-ui';
  29. import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
  30. import { CreditCard, Gift, Link as LinkIcon, Coins } from 'lucide-react';
  31. import { IconGift } from '@douyinfe/semi-icons';
  32. import RightStatsCard from './RightStatsCard';
  33. const { Text } = Typography;
  34. const RechargeCard = ({
  35. t,
  36. enableOnlineTopUp,
  37. enableStripeTopUp,
  38. presetAmounts,
  39. selectedPreset,
  40. selectPresetAmount,
  41. formatLargeNumber,
  42. priceRatio,
  43. topUpCount,
  44. minTopUp,
  45. renderQuotaWithAmount,
  46. getAmount,
  47. setTopUpCount,
  48. setSelectedPreset,
  49. renderAmount,
  50. amountLoading,
  51. payMethods,
  52. preTopUp,
  53. paymentLoading,
  54. payWay,
  55. redemptionCode,
  56. setRedemptionCode,
  57. topUp,
  58. isSubmitting,
  59. topUpLink,
  60. openTopUpLink,
  61. // 新增:用于右侧统计卡片
  62. userState,
  63. renderQuota,
  64. statusLoading,
  65. }) => {
  66. return (
  67. <Card className="!rounded-2xl shadow-sm border-0">
  68. {/* 卡片头部 */}
  69. <div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4 gap-3">
  70. <div className="flex items-center">
  71. <Avatar size="small" color="blue" className="mr-3 shadow-md">
  72. <CreditCard size={16} />
  73. </Avatar>
  74. <div>
  75. <Typography.Text className="text-lg font-medium">{t('账户充值')}</Typography.Text>
  76. <div className="text-xs text-gray-600 dark:text-gray-400">{t('多种充值方式,安全便捷')}</div>
  77. </div>
  78. </div>
  79. <RightStatsCard t={t} userState={userState} renderQuota={renderQuota} />
  80. </div>
  81. <Tabs type="card" defaultActiveKey="online">
  82. {/* 在线充值 Tab */}
  83. <TabPane
  84. tab={
  85. <div className="flex items-center">
  86. <CreditCard size={16} className="mr-2" />
  87. {t('在线充值')}
  88. </div>
  89. }
  90. itemKey="online"
  91. >
  92. <div className="py-4">
  93. {statusLoading ? (
  94. <div className='py-8 flex justify-center'>
  95. <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500'></div>
  96. </div>
  97. ) : (enableOnlineTopUp || enableStripeTopUp) ? (
  98. <div className='space-y-6'>
  99. {/* 预设充值额度选择 */}
  100. {(enableOnlineTopUp || enableStripeTopUp) && (
  101. <div>
  102. <Text strong className='block mb-3'>
  103. {t('选择充值额度')}
  104. </Text>
  105. <div className='grid grid-cols-2 sm:grid-cols-4 gap-3'>
  106. {presetAmounts.map((preset, index) => (
  107. <Card
  108. key={index}
  109. onClick={() => selectPresetAmount(preset)}
  110. className={`cursor-pointer !rounded-xl transition-all hover:shadow-md ${selectedPreset === preset.value
  111. ? 'border-blue-500 shadow-md'
  112. : 'border-slate-200 hover:border-slate-300 dark:border-slate-600 dark:hover:border-slate-500'
  113. }`}
  114. bodyStyle={{ textAlign: 'center', padding: '12px' }}
  115. >
  116. <div className='font-medium text-lg flex items-center justify-center mb-1'>
  117. <Coins size={16} className='mr-1' />
  118. {formatLargeNumber(preset.value)}
  119. </div>
  120. <div className='text-xs text-gray-500 dark:text-gray-400'>
  121. {t('实付')} ¥{(preset.value * priceRatio).toFixed(2)}
  122. </div>
  123. </Card>
  124. ))}
  125. </div>
  126. </div>
  127. )}
  128. {/* 自定义充值金额 */}
  129. {(enableOnlineTopUp || enableStripeTopUp) && (
  130. <div className='space-y-4'>
  131. <Divider style={{ margin: '24px 0' }}>
  132. <Text className='text-sm font-medium text-slate-600 dark:text-slate-400'>
  133. {t('或输入自定义金额')}
  134. </Text>
  135. </Divider>
  136. <div>
  137. <div className='flex justify-between mb-2'>
  138. <Text strong className='text-slate-700 dark:text-slate-200'>{t('充值数量')}</Text>
  139. {amountLoading ? (
  140. <Skeleton.Title style={{ width: '80px', height: '16px' }} />
  141. ) : (
  142. <Text className='text-red-600 font-semibold'>
  143. {t('实付金额:')}<span className='font-bold' style={{ color: 'red' }}>{renderAmount()}</span>
  144. </Text>
  145. )}
  146. </div>
  147. <InputNumber
  148. disabled={!enableOnlineTopUp && !enableStripeTopUp}
  149. placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
  150. value={topUpCount}
  151. min={minTopUp}
  152. max={999999999}
  153. step={1}
  154. precision={0}
  155. onChange={async (value) => {
  156. if (value && value >= 1) {
  157. setTopUpCount(value);
  158. setSelectedPreset(null);
  159. await getAmount(value);
  160. }
  161. }}
  162. onBlur={(e) => {
  163. const value = parseInt(e.target.value);
  164. if (!value || value < 1) {
  165. setTopUpCount(1);
  166. getAmount(1);
  167. }
  168. }}
  169. className='w-full !rounded-lg'
  170. formatter={(value) => (value ? `${value}` : '')}
  171. parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
  172. />
  173. </div>
  174. {/* 支付方式选择 */}
  175. <div>
  176. <Text strong className='block mb-3 text-slate-700 dark:text-slate-200'>
  177. {t('选择支付方式')}
  178. </Text>
  179. <div className={`grid gap-3 ${payMethods.length <= 2
  180. ? 'grid-cols-1 sm:grid-cols-2'
  181. : payMethods.length === 3
  182. ? 'grid-cols-1 sm:grid-cols-3'
  183. : 'grid-cols-2 sm:grid-cols-4'
  184. }`}>
  185. {payMethods.map((payMethod) => (
  186. <Card
  187. key={payMethod.type}
  188. onClick={() => preTopUp(payMethod.type)}
  189. className={`cursor-pointer !rounded-xl transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type
  190. ? 'border-blue-500 shadow-md'
  191. : 'border-slate-200 hover:border-slate-300 dark:border-slate-600 dark:hover:border-slate-500'
  192. } ${(!enableOnlineTopUp && payMethod.type !== 'stripe') ||
  193. (!enableStripeTopUp && payMethod.type === 'stripe')
  194. ? 'opacity-50 cursor-not-allowed'
  195. : ''
  196. }`}
  197. bodyStyle={{ padding: '12px', textAlign: 'center' }}
  198. >
  199. {paymentLoading && payWay === payMethod.type ? (
  200. <div className='flex flex-col items-center justify-center'>
  201. <div className='mb-2'>
  202. <div className='animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500'></div>
  203. </div>
  204. <div className='text-xs text-slate-500 dark:text-slate-400'>{t('处理中')}</div>
  205. </div>
  206. ) : (
  207. <>
  208. <div className='flex items-center justify-center mb-2'>
  209. {payMethod.type === 'zfb' ? (
  210. <SiAlipay size={24} color="#1677FF" />
  211. ) : payMethod.type === 'wx' ? (
  212. <SiWechat size={24} color="#07C160" />
  213. ) : payMethod.type === 'stripe' ? (
  214. <SiStripe size={24} color="#635BFF" />
  215. ) : (
  216. <CreditCard size={24} className='text-slate-500' />
  217. )}
  218. </div>
  219. <div className='text-sm font-medium text-slate-700 dark:text-slate-200'>{payMethod.name}</div>
  220. </>
  221. )}
  222. </Card>
  223. ))}
  224. </div>
  225. </div>
  226. </div>
  227. )}
  228. </div>
  229. ) : (
  230. <div className='py-8'>
  231. <Banner
  232. type='warning'
  233. description={t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
  234. className='!rounded-xl'
  235. closeIcon={null}
  236. />
  237. </div>
  238. )}
  239. </div>
  240. </TabPane>
  241. {/* 兑换码充值 Tab */}
  242. <TabPane
  243. tab={
  244. <div className="flex items-center">
  245. <Gift size={16} className="mr-2" />
  246. {t('兑换码充值')}
  247. </div>
  248. }
  249. itemKey="redeem"
  250. >
  251. <div className="py-4">
  252. <div className='space-y-4'>
  253. <Input
  254. placeholder={t('请输入兑换码')}
  255. value={redemptionCode}
  256. onChange={(value) => setRedemptionCode(value)}
  257. className='!rounded-lg'
  258. prefix={<IconGift />}
  259. />
  260. <div className='flex flex-col sm:flex-row gap-2'>
  261. {topUpLink && (
  262. <Button
  263. type='secondary'
  264. theme='outline'
  265. onClick={openTopUpLink}
  266. className='flex-1 !rounded-lg !border-slate-300 !text-slate-600 hover:!border-slate-400 hover:!text-slate-700'
  267. icon={<LinkIcon size={16} />}
  268. >
  269. {t('获取兑换码')}
  270. </Button>
  271. )}
  272. <Button
  273. type='primary'
  274. theme='solid'
  275. onClick={topUp}
  276. disabled={isSubmitting || !redemptionCode}
  277. loading={isSubmitting}
  278. className='flex-1 !rounded-lg !bg-slate-600 hover:!bg-slate-700'
  279. >
  280. {isSubmitting ? t('兑换中...') : t('兑换')}
  281. </Button>
  282. </div>
  283. </div>
  284. </div>
  285. </TabPane>
  286. </Tabs>
  287. </Card>
  288. );
  289. };
  290. export default RechargeCard;