| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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, |
| Receipt, |
| } from 'lucide-react'; |
| import { IconGift } from '@douyinfe/semi-icons'; |
| import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime'; |
| import { getCurrencyConfig } from '../../helpers/render'; |
|
|
| const { Text } = Typography; |
|
|
| const RechargeCard = ({ |
| t, |
| enableOnlineTopUp, |
| enableStripeTopUp, |
| enableCreemTopUp, |
| creemProducts, |
| creemPreTopUp, |
| 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, |
| onOpenHistory, |
| }) => { |
| const onlineFormApiRef = useRef(null); |
| const redeemFormApiRef = useRef(null); |
| const showAmountSkeleton = useMinimumLoadingTime(amountLoading); |
| console.log(' enabled screem ?', enableCreemTopUp, ' products ?', creemProducts); |
| return ( |
| <Card className='!rounded-2xl shadow-sm border-0'> |
| {/* 卡片头部 */} |
| <div className='flex items-center justify-between mb-4'> |
| <div className='flex items-center'> |
| <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> |
| <Button |
| icon={<Receipt size={16} />} |
| theme='solid' |
| onClick={onOpenHistory} |
| > |
| {t('账单')} |
| </Button> |
| </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 || enableCreemTopUp ? ( |
| <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={ |
| <div className='flex items-center gap-2'> |
| <span>{t('选择充值额度')}</span> |
| {(() => { |
| const { symbol, rate, type } = getCurrencyConfig(); |
| if (type === 'USD') return null; |
| |
| return ( |
| <span |
| style={{ |
| color: 'var(--semi-color-text-2)', |
| fontSize: '12px', |
| fontWeight: 'normal', |
| }} |
| > |
| (1 $ = {rate.toFixed(2)} {symbol}) |
| </span> |
| ); |
| })()} |
| </div> |
| } |
| > |
| <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; |
| |
| // 根据当前货币类型换算显示金额和数量 |
| const { symbol, rate, type } = getCurrencyConfig(); |
| const statusStr = localStorage.getItem('status'); |
| let usdRate = 7; // 默认CNY汇率 |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| usdRate = s?.usd_exchange_rate || 7; |
| } |
| } catch (e) {} |
| |
| let displayValue = preset.value; // 显示的数量 |
| let displayActualPay = actualPay; |
| let displaySave = save; |
| |
| if (type === 'USD') { |
| // 数量保持USD,价格从CNY转USD |
| displayActualPay = actualPay / usdRate; |
| displaySave = save / usdRate; |
| } else if (type === 'CNY') { |
| // 数量转CNY,价格已是CNY |
| displayValue = preset.value * usdRate; |
| } else if (type === 'CUSTOM') { |
| // 数量和价格都转自定义货币 |
| displayValue = preset.value * rate; |
| displayActualPay = (actualPay / usdRate) * rate; |
| displaySave = (save / usdRate) * rate; |
| } |
| |
| 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(displayValue)} {symbol} |
| {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('实付')} {symbol} |
| {displayActualPay.toFixed(2)}, |
| {hasDiscount |
| ? `${t('节省')} ${symbol}${displaySave.toFixed(2)}` |
| : `${t('节省')} ${symbol}0.00`} |
| </div> |
| </div> |
| </Card> |
| ); |
| })} |
| </div> |
| </Form.Slot> |
| )} |
| |
| {/* Creem 充值区域 */} |
| {enableCreemTopUp && creemProducts.length > 0 && ( |
| <Form.Slot label={t('Creem 充值')}> |
| <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3'> |
| {creemProducts.map((product, index) => ( |
| <Card |
| key={index} |
| onClick={() => creemPreTopUp(product)} |
| className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300' |
| bodyStyle={{ textAlign: 'center', padding: '16px' }} |
| > |
| <div className='font-medium text-lg mb-2'> |
| {product.name} |
| </div> |
| <div className='text-sm text-gray-600 mb-2'> |
| {t('充值额度')}: {product.quota} |
| </div> |
| <div className='text-lg font-semibold text-blue-600'> |
| {product.currency === 'EUR' ? '€' : '$'}{product.price} |
| </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; |
|
|