| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- /*
- 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,
- Card,
- Button,
- Input,
- InputNumber,
- Banner,
- Skeleton,
- Divider,
- Form,
- Space,
- Row,
- Col,
- Spin,
- } from '@douyinfe/semi-ui';
- import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
- import { CreditCard, Gift, Link as LinkIcon, 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,
- }) => {
- 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-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-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-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('选择支付方式')}>
- <Space wrap>
- {payMethods.map((payMethod) => (
- <Button
- key={payMethod.type}
- theme='outline'
- type='tertiary'
- onClick={() => preTopUp(payMethod.type)}
- disabled={(!enableOnlineTopUp && payMethod.type !== 'stripe') ||
- (!enableStripeTopUp && payMethod.type === 'stripe')}
- 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)'} />
- )
- }
- >
- {payMethod.name}
- </Button>
- ))}
- </Space>
- </Form.Slot>
- </Col>
- </Row>
- )}
- {(enableOnlineTopUp || enableStripeTopUp) && (
- <Form.Slot label={t('选择充值额度')}>
- <Space wrap>
- {presetAmounts.map((preset, index) => (
- <Button
- key={index}
- theme={selectedPreset === preset.value ? 'solid' : 'outline'}
- type={selectedPreset === preset.value ? 'primary' : 'tertiary'}
- onClick={() => {
- selectPresetAmount(preset);
- onlineFormApiRef.current?.setValue('topUpCount', preset.value);
- }}
- className='!rounded-lg !py-2 !px-3'
- >
- <div className='flex items-center gap-2'>
- <Coins size={14} className='opacity-80' />
- <span className='font-medium'>{formatLargeNumber(preset.value)}</span>
- <span className='text-xs text-gray-500'>¥{(preset.value * priceRatio).toFixed(2)}</span>
- </div>
- </Button>
- ))}
- </Space>
- </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;
|