Spaces:
Build error
Build error
| /* | |
| Copyright (C) 2025 QuantumNous | |
| This program is free software: you can redistribute it and/or modify | |
| it under the terms of the GNU Affero General Public License as | |
| published by the Free Software Foundation, either version 3 of the | |
| License, or (at your option) any later version. | |
| This program is distributed in the hope that it will be useful, | |
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| GNU Affero General Public License for more details. | |
| You should have received a copy of the GNU Affero General Public License | |
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| For commercial licensing, please contact support@quantumnous.com | |
| */ | |
| import React, { useRef } from 'react'; | |
| import { | |
| Avatar, | |
| Typography, | |
| Tag, | |
| Card, | |
| Button, | |
| Banner, | |
| Skeleton, | |
| Form, | |
| Space, | |
| Row, | |
| Col, | |
| Spin, Tooltip | |
| } from '@douyinfe/semi-ui'; | |
| import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; | |
| import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react'; | |
| import { IconGift } from '@douyinfe/semi-icons'; | |
| import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime'; | |
| const { Text } = Typography; | |
| const RechargeCard = ({ | |
| t, | |
| enableOnlineTopUp, | |
| enableStripeTopUp, | |
| presetAmounts, | |
| selectedPreset, | |
| selectPresetAmount, | |
| formatLargeNumber, | |
| priceRatio, | |
| topUpCount, | |
| minTopUp, | |
| renderQuotaWithAmount, | |
| getAmount, | |
| setTopUpCount, | |
| setSelectedPreset, | |
| renderAmount, | |
| amountLoading, | |
| payMethods, | |
| preTopUp, | |
| paymentLoading, | |
| payWay, | |
| redemptionCode, | |
| setRedemptionCode, | |
| topUp, | |
| isSubmitting, | |
| topUpLink, | |
| openTopUpLink, | |
| userState, | |
| renderQuota, | |
| statusLoading, | |
| topupInfo, | |
| }) => { | |
| const onlineFormApiRef = useRef(null); | |
| const redeemFormApiRef = useRef(null); | |
| const showAmountSkeleton = useMinimumLoadingTime(amountLoading); | |
| return ( | |
| <Card className='!rounded-2xl shadow-sm border-0'> | |
| {/* 卡片头部 */} | |
| <div className='flex items-center mb-4'> | |
| <Avatar size='small' color='blue' className='mr-3 shadow-md'> | |
| <CreditCard size={16} /> | |
| </Avatar> | |
| <div> | |
| <Typography.Text className='text-lg font-medium'> | |
| {t('账户充值')} | |
| </Typography.Text> | |
| <div className='text-xs'>{t('多种充值方式,安全便捷')}</div> | |
| </div> | |
| </div> | |
| <Space vertical style={{ width: '100%' }}> | |
| {/* 统计数据 */} | |
| <Card | |
| className='!rounded-xl w-full' | |
| cover={ | |
| <div | |
| className='relative h-30' | |
| style={{ | |
| '--palette-primary-darkerChannel': '37 99 235', | |
| backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`, | |
| backgroundSize: 'cover', | |
| backgroundPosition: 'center', | |
| backgroundRepeat: 'no-repeat', | |
| }} | |
| > | |
| <div className='relative z-10 h-full flex flex-col justify-between p-4'> | |
| <div className='flex justify-between items-center'> | |
| <Text strong style={{ color: 'white', fontSize: '16px' }}> | |
| {t('账户统计')} | |
| </Text> | |
| </div> | |
| {/* 统计数据 */} | |
| <div className='grid grid-cols-3 gap-6 mt-4'> | |
| {/* 当前余额 */} | |
| <div className='text-center'> | |
| <div | |
| className='text-base sm:text-2xl font-bold mb-2' | |
| style={{ color: 'white' }} | |
| > | |
| {renderQuota(userState?.user?.quota)} | |
| </div> | |
| <div className='flex items-center justify-center text-sm'> | |
| <Wallet | |
| size={14} | |
| className='mr-1' | |
| style={{ color: 'rgba(255,255,255,0.8)' }} | |
| /> | |
| <Text | |
| style={{ | |
| color: 'rgba(255,255,255,0.8)', | |
| fontSize: '12px', | |
| }} | |
| > | |
| {t('当前余额')} | |
| </Text> | |
| </div> | |
| </div> | |
| {/* 历史消耗 */} | |
| <div className='text-center'> | |
| <div | |
| className='text-base sm:text-2xl font-bold mb-2' | |
| style={{ color: 'white' }} | |
| > | |
| {renderQuota(userState?.user?.used_quota)} | |
| </div> | |
| <div className='flex items-center justify-center text-sm'> | |
| <TrendingUp | |
| size={14} | |
| className='mr-1' | |
| style={{ color: 'rgba(255,255,255,0.8)' }} | |
| /> | |
| <Text | |
| style={{ | |
| color: 'rgba(255,255,255,0.8)', | |
| fontSize: '12px', | |
| }} | |
| > | |
| {t('历史消耗')} | |
| </Text> | |
| </div> | |
| </div> | |
| {/* 请求次数 */} | |
| <div className='text-center'> | |
| <div | |
| className='text-base sm:text-2xl font-bold mb-2' | |
| style={{ color: 'white' }} | |
| > | |
| {userState?.user?.request_count || 0} | |
| </div> | |
| <div className='flex items-center justify-center text-sm'> | |
| <BarChart2 | |
| size={14} | |
| className='mr-1' | |
| style={{ color: 'rgba(255,255,255,0.8)' }} | |
| /> | |
| <Text | |
| style={{ | |
| color: 'rgba(255,255,255,0.8)', | |
| fontSize: '12px', | |
| }} | |
| > | |
| {t('请求次数')} | |
| </Text> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| > | |
| {/* 在线充值表单 */} | |
| {statusLoading ? ( | |
| <div className='py-8 flex justify-center'> | |
| <Spin size='large' /> | |
| </div> | |
| ) : enableOnlineTopUp || enableStripeTopUp ? ( | |
| <Form | |
| getFormApi={(api) => (onlineFormApiRef.current = api)} | |
| initValues={{ topUpCount: topUpCount }} | |
| > | |
| <div className='space-y-6'> | |
| {(enableOnlineTopUp || enableStripeTopUp) && ( | |
| <Row gutter={12}> | |
| <Col xs={24} sm={24} md={24} lg={10} xl={10}> | |
| <Form.InputNumber | |
| field='topUpCount' | |
| label={t('充值数量')} | |
| disabled={!enableOnlineTopUp && !enableStripeTopUp} | |
| placeholder={ | |
| t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) | |
| } | |
| value={topUpCount} | |
| min={minTopUp} | |
| max={999999999} | |
| step={1} | |
| precision={0} | |
| onChange={async (value) => { | |
| if (value && value >= 1) { | |
| setTopUpCount(value); | |
| setSelectedPreset(null); | |
| await getAmount(value); | |
| } | |
| }} | |
| onBlur={(e) => { | |
| const value = parseInt(e.target.value); | |
| if (!value || value < 1) { | |
| setTopUpCount(1); | |
| getAmount(1); | |
| } | |
| }} | |
| formatter={(value) => (value ? `${value}` : '')} | |
| parser={(value) => | |
| value ? parseInt(value.replace(/[^\d]/g, '')) : 0 | |
| } | |
| extraText={ | |
| <Skeleton | |
| loading={showAmountSkeleton} | |
| active | |
| placeholder={ | |
| <Skeleton.Title | |
| style={{ | |
| width: 120, | |
| height: 20, | |
| borderRadius: 6, | |
| }} | |
| /> | |
| } | |
| > | |
| <Text type='secondary' className='text-red-600'> | |
| {t('实付金额:')} | |
| <span style={{ color: 'red' }}> | |
| {renderAmount()} | |
| </span> | |
| </Text> | |
| </Skeleton> | |
| } | |
| style={{ width: '100%' }} | |
| /> | |
| </Col> | |
| <Col xs={24} sm={24} md={24} lg={14} xl={14}> | |
| <Form.Slot label={t('选择支付方式')}> | |
| {payMethods && payMethods.length > 0 ? ( | |
| <Space wrap> | |
| {payMethods.map((payMethod) => { | |
| const minTopupVal = Number(payMethod.min_topup) || 0; | |
| const isStripe = payMethod.type === 'stripe'; | |
| const disabled = | |
| (!enableOnlineTopUp && !isStripe) || | |
| (!enableStripeTopUp && isStripe) || | |
| minTopupVal > Number(topUpCount || 0); | |
| const buttonEl = ( | |
| <Button | |
| key={payMethod.type} | |
| theme='outline' | |
| type='tertiary' | |
| onClick={() => preTopUp(payMethod.type)} | |
| disabled={disabled} | |
| loading={paymentLoading && payWay === payMethod.type} | |
| icon={ | |
| payMethod.type === 'alipay' ? ( | |
| <SiAlipay size={18} color='#1677FF' /> | |
| ) : payMethod.type === 'wxpay' ? ( | |
| <SiWechat size={18} color='#07C160' /> | |
| ) : payMethod.type === 'stripe' ? ( | |
| <SiStripe size={18} color='#635BFF' /> | |
| ) : ( | |
| <CreditCard | |
| size={18} | |
| color={payMethod.color || 'var(--semi-color-text-2)'} | |
| /> | |
| ) | |
| } | |
| className='!rounded-lg !px-4 !py-2' | |
| > | |
| {payMethod.name} | |
| </Button> | |
| ); | |
| return disabled && minTopupVal > Number(topUpCount || 0) ? ( | |
| <Tooltip content={t('此支付方式最低充值金额为') + ' ' + minTopupVal} key={payMethod.type}> | |
| {buttonEl} | |
| </Tooltip> | |
| ) : ( | |
| <React.Fragment key={payMethod.type}>{buttonEl}</React.Fragment> | |
| ); | |
| })} | |
| </Space> | |
| ) : ( | |
| <div className='text-gray-500 text-sm p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300'> | |
| {t('暂无可用的支付方式,请联系管理员配置')} | |
| </div> | |
| )} | |
| </Form.Slot> | |
| </Col> | |
| </Row> | |
| )} | |
| {(enableOnlineTopUp || enableStripeTopUp) && ( | |
| <Form.Slot label={t('选择充值额度')}> | |
| <div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'> | |
| {presetAmounts.map((preset, index) => { | |
| const discount = preset.discount || topupInfo?.discount?.[preset.value] || 1.0; | |
| const originalPrice = preset.value * priceRatio; | |
| const discountedPrice = originalPrice * discount; | |
| const hasDiscount = discount < 1.0; | |
| const actualPay = discountedPrice; | |
| const save = originalPrice - discountedPrice; | |
| return ( | |
| <Card | |
| key={index} | |
| style={{ | |
| cursor: 'pointer', | |
| border: selectedPreset === preset.value | |
| ? '2px solid var(--semi-color-primary)' | |
| : '1px solid var(--semi-color-border)', | |
| height: '100%', | |
| width: '100%' | |
| }} | |
| bodyStyle={{ padding: '12px' }} | |
| onClick={() => { | |
| selectPresetAmount(preset); | |
| onlineFormApiRef.current?.setValue( | |
| 'topUpCount', | |
| preset.value, | |
| ); | |
| }} | |
| > | |
| <div style={{ textAlign: 'center' }}> | |
| <Typography.Title heading={6} style={{ margin: '0 0 8px 0' }}> | |
| <Coins size={18} /> | |
| {formatLargeNumber(preset.value)} | |
| {hasDiscount && ( | |
| <Tag style={{ marginLeft: 4 }} color="green"> | |
| {t('折').includes('off') ? | |
| ((1 - parseFloat(discount)) * 100).toFixed(1) : | |
| (discount * 10).toFixed(1)}{t('折')} | |
| </Tag> | |
| )} | |
| </Typography.Title> | |
| <div style={{ | |
| color: 'var(--semi-color-text-2)', | |
| fontSize: '12px', | |
| margin: '4px 0' | |
| }}> | |
| {t('实付')} {actualPay.toFixed(2)}, | |
| {hasDiscount ? `${t('节省')} ${save.toFixed(2)}` : `${t('节省')} 0.00`} | |
| </div> | |
| </div> | |
| </Card> | |
| ); | |
| })} | |
| </div> | |
| </Form.Slot> | |
| )} | |
| </div> | |
| </Form> | |
| ) : ( | |
| <Banner | |
| type='info' | |
| description={t( | |
| '管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。', | |
| )} | |
| className='!rounded-xl' | |
| closeIcon={null} | |
| /> | |
| )} | |
| </Card> | |
| {/* 兑换码充值 */} | |
| <Card | |
| className='!rounded-xl w-full' | |
| title={ | |
| <Text type='tertiary' strong> | |
| {t('兑换码充值')} | |
| </Text> | |
| } | |
| > | |
| <Form | |
| getFormApi={(api) => (redeemFormApiRef.current = api)} | |
| initValues={{ redemptionCode: redemptionCode }} | |
| > | |
| <Form.Input | |
| field='redemptionCode' | |
| noLabel={true} | |
| placeholder={t('请输入兑换码')} | |
| value={redemptionCode} | |
| onChange={(value) => setRedemptionCode(value)} | |
| prefix={<IconGift />} | |
| suffix={ | |
| <div className='flex items-center gap-2'> | |
| <Button | |
| type='primary' | |
| theme='solid' | |
| onClick={topUp} | |
| loading={isSubmitting} | |
| > | |
| {t('兑换额度')} | |
| </Button> | |
| </div> | |
| } | |
| showClear | |
| style={{ width: '100%' }} | |
| extraText={ | |
| topUpLink && ( | |
| <Text type='tertiary'> | |
| {t('在找兑换码?')} | |
| <Text | |
| type='secondary' | |
| underline | |
| className='cursor-pointer' | |
| onClick={openTopUpLink} | |
| > | |
| {t('购买兑换码')} | |
| </Text> | |
| </Text> | |
| ) | |
| } | |
| /> | |
| </Form> | |
| </Card> | |
| </Space> | |
| </Card> | |
| ); | |
| }; | |
| export default RechargeCard; | |