| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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; |
| |
|