| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useEffect, useState, useContext, useRef } from 'react'; |
| import { |
| API, |
| showError, |
| showInfo, |
| showSuccess, |
| renderQuota, |
| renderQuotaWithAmount, |
| copy, |
| getQuotaPerUnit, |
| } from '../../helpers'; |
| import { Modal, Toast } from '@douyinfe/semi-ui'; |
| import { useTranslation } from 'react-i18next'; |
| import { UserContext } from '../../context/User'; |
| import { StatusContext } from '../../context/Status'; |
|
|
| import RechargeCard from './RechargeCard'; |
| import InvitationCard from './InvitationCard'; |
| import TransferModal from './modals/TransferModal'; |
| import PaymentConfirmModal from './modals/PaymentConfirmModal'; |
| import TopupHistoryModal from './modals/TopupHistoryModal'; |
|
|
| const TopUp = () => { |
| const { t } = useTranslation(); |
| const [userState, userDispatch] = useContext(UserContext); |
| const [statusState] = useContext(StatusContext); |
|
|
| const [redemptionCode, setRedemptionCode] = useState(''); |
| const [amount, setAmount] = useState(0.0); |
| const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1); |
| const [topUpCount, setTopUpCount] = useState( |
| statusState?.status?.min_topup || 1, |
| ); |
| const [topUpLink, setTopUpLink] = useState( |
| statusState?.status?.top_up_link || '', |
| ); |
| const [enableOnlineTopUp, setEnableOnlineTopUp] = useState( |
| statusState?.status?.enable_online_topup || false, |
| ); |
| const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1); |
|
|
| const [enableStripeTopUp, setEnableStripeTopUp] = useState( |
| statusState?.status?.enable_stripe_topup || false, |
| ); |
| const [statusLoading, setStatusLoading] = useState(true); |
|
|
| |
| const [creemProducts, setCreemProducts] = useState([]); |
| const [enableCreemTopUp, setEnableCreemTopUp] = useState(false); |
| const [creemOpen, setCreemOpen] = useState(false); |
| const [selectedCreemProduct, setSelectedCreemProduct] = useState(null); |
|
|
| const [isSubmitting, setIsSubmitting] = useState(false); |
| const [open, setOpen] = useState(false); |
| const [payWay, setPayWay] = useState(''); |
| const [amountLoading, setAmountLoading] = useState(false); |
| const [paymentLoading, setPaymentLoading] = useState(false); |
| const [confirmLoading, setConfirmLoading] = useState(false); |
| const [payMethods, setPayMethods] = useState([]); |
|
|
| const affFetchedRef = useRef(false); |
|
|
| |
| const [affLink, setAffLink] = useState(''); |
| const [openTransfer, setOpenTransfer] = useState(false); |
| const [transferAmount, setTransferAmount] = useState(0); |
|
|
| |
| const [openHistory, setOpenHistory] = useState(false); |
|
|
| |
| const [presetAmounts, setPresetAmounts] = useState([]); |
| const [selectedPreset, setSelectedPreset] = useState(null); |
|
|
| |
| const [topupInfo, setTopupInfo] = useState({ |
| amount_options: [], |
| discount: {}, |
| }); |
|
|
| const topUp = async () => { |
| if (redemptionCode === '') { |
| showInfo(t('请输入兑换码!')); |
| return; |
| } |
| setIsSubmitting(true); |
| try { |
| const res = await API.post('/api/user/topup', { |
| key: redemptionCode, |
| }); |
| const { success, message, data } = res.data; |
| if (success) { |
| showSuccess(t('兑换成功!')); |
| Modal.success({ |
| title: t('兑换成功!'), |
| content: t('成功兑换额度:') + renderQuota(data), |
| centered: true, |
| }); |
| if (userState.user) { |
| const updatedUser = { |
| ...userState.user, |
| quota: userState.user.quota + data, |
| }; |
| userDispatch({ type: 'login', payload: updatedUser }); |
| } |
| setRedemptionCode(''); |
| } else { |
| showError(message); |
| } |
| } catch (err) { |
| showError(t('请求失败')); |
| } finally { |
| setIsSubmitting(false); |
| } |
| }; |
|
|
| const openTopUpLink = () => { |
| if (!topUpLink) { |
| showError(t('超级管理员未设置充值链接!')); |
| return; |
| } |
| window.open(topUpLink, '_blank'); |
| }; |
|
|
| const preTopUp = async (payment) => { |
| if (payment === 'stripe') { |
| if (!enableStripeTopUp) { |
| showError(t('管理员未开启Stripe充值!')); |
| return; |
| } |
| } else { |
| if (!enableOnlineTopUp) { |
| showError(t('管理员未开启在线充值!')); |
| return; |
| } |
| } |
|
|
| setPayWay(payment); |
| setPaymentLoading(true); |
| try { |
| if (payment === 'stripe') { |
| await getStripeAmount(); |
| } else { |
| await getAmount(); |
| } |
|
|
| if (topUpCount < minTopUp) { |
| showError(t('充值数量不能小于') + minTopUp); |
| return; |
| } |
| setOpen(true); |
| } catch (error) { |
| showError(t('获取金额失败')); |
| } finally { |
| setPaymentLoading(false); |
| } |
| }; |
|
|
| const onlineTopUp = async () => { |
| if (payWay === 'stripe') { |
| |
| if (amount === 0) { |
| await getStripeAmount(); |
| } |
| } else { |
| |
| if (amount === 0) { |
| await getAmount(); |
| } |
| } |
|
|
| if (topUpCount < minTopUp) { |
| showError('充值数量不能小于' + minTopUp); |
| return; |
| } |
| setConfirmLoading(true); |
| try { |
| let res; |
| if (payWay === 'stripe') { |
| |
| res = await API.post('/api/user/stripe/pay', { |
| amount: parseInt(topUpCount), |
| payment_method: 'stripe', |
| }); |
| } else { |
| |
| res = await API.post('/api/user/pay', { |
| amount: parseInt(topUpCount), |
| payment_method: payWay, |
| }); |
| } |
|
|
| if (res !== undefined) { |
| const { message, data } = res.data; |
| if (message === 'success') { |
| if (payWay === 'stripe') { |
| |
| window.open(data.pay_link, '_blank'); |
| } else { |
| |
| let params = data; |
| let url = res.data.url; |
| let form = document.createElement('form'); |
| form.action = url; |
| form.method = 'POST'; |
| let isSafari = |
| navigator.userAgent.indexOf('Safari') > -1 && |
| navigator.userAgent.indexOf('Chrome') < 1; |
| if (!isSafari) { |
| form.target = '_blank'; |
| } |
| for (let key in params) { |
| let input = document.createElement('input'); |
| input.type = 'hidden'; |
| input.name = key; |
| input.value = params[key]; |
| form.appendChild(input); |
| } |
| document.body.appendChild(form); |
| form.submit(); |
| document.body.removeChild(form); |
| } |
| } else { |
| showError(data); |
| } |
| } else { |
| showError(res); |
| } |
| } catch (err) { |
| console.log(err); |
| showError(t('支付请求失败')); |
| } finally { |
| setOpen(false); |
| setConfirmLoading(false); |
| } |
| }; |
|
|
| const creemPreTopUp = async (product) => { |
| if (!enableCreemTopUp) { |
| showError(t('管理员未开启 Creem 充值!')); |
| return; |
| } |
| setSelectedCreemProduct(product); |
| setCreemOpen(true); |
| }; |
|
|
| const onlineCreemTopUp = async () => { |
| if (!selectedCreemProduct) { |
| showError(t('请选择产品')); |
| return; |
| } |
| |
| if (!selectedCreemProduct.productId) { |
| showError(t('产品配置错误,请联系管理员')); |
| return; |
| } |
| setConfirmLoading(true); |
| try { |
| const res = await API.post('/api/user/creem/pay', { |
| product_id: selectedCreemProduct.productId, |
| payment_method: 'creem', |
| }); |
| if (res !== undefined) { |
| const { message, data } = res.data; |
| if (message === 'success') { |
| processCreemCallback(data); |
| } else { |
| showError(data); |
| } |
| } else { |
| showError(res); |
| } |
| } catch (err) { |
| console.log(err); |
| showError(t('支付请求失败')); |
| } finally { |
| setCreemOpen(false); |
| setConfirmLoading(false); |
| } |
| }; |
|
|
| const processCreemCallback = (data) => { |
| |
| window.open(data.checkout_url, '_blank'); |
| }; |
|
|
| const getUserQuota = async () => { |
| let res = await API.get(`/api/user/self`); |
| const { success, message, data } = res.data; |
| if (success) { |
| userDispatch({ type: 'login', payload: data }); |
| } else { |
| showError(message); |
| } |
| }; |
|
|
| |
| const getTopupInfo = async () => { |
| try { |
| const res = await API.get('/api/user/topup/info'); |
| const { message, data, success } = res.data; |
| if (success) { |
| setTopupInfo({ |
| amount_options: data.amount_options || [], |
| discount: data.discount || {}, |
| }); |
|
|
| |
| let payMethods = data.pay_methods || []; |
| try { |
| if (typeof payMethods === 'string') { |
| payMethods = JSON.parse(payMethods); |
| } |
| if (payMethods && payMethods.length > 0) { |
| |
| payMethods = payMethods.filter((method) => { |
| return method.name && method.type; |
| }); |
| |
| payMethods = payMethods.map((method) => { |
| |
| const normalizedMinTopup = Number(method.min_topup); |
| method.min_topup = Number.isFinite(normalizedMinTopup) |
| ? normalizedMinTopup |
| : 0; |
|
|
| |
| if ( |
| method.type === 'stripe' && |
| (!method.min_topup || method.min_topup <= 0) |
| ) { |
| const stripeMin = Number(data.stripe_min_topup); |
| if (Number.isFinite(stripeMin)) { |
| method.min_topup = stripeMin; |
| } |
| } |
|
|
| if (!method.color) { |
| if (method.type === 'alipay') { |
| method.color = 'rgba(var(--semi-blue-5), 1)'; |
| } else if (method.type === 'wxpay') { |
| method.color = 'rgba(var(--semi-green-5), 1)'; |
| } else if (method.type === 'stripe') { |
| method.color = 'rgba(var(--semi-purple-5), 1)'; |
| } else { |
| method.color = 'rgba(var(--semi-primary-5), 1)'; |
| } |
| } |
| return method; |
| }); |
| } else { |
| payMethods = []; |
| } |
|
|
| |
| |
|
|
| setPayMethods(payMethods); |
| const enableStripeTopUp = data.enable_stripe_topup || false; |
| const enableOnlineTopUp = data.enable_online_topup || false; |
| const enableCreemTopUp = data.enable_creem_topup || false; |
| const minTopUpValue = enableOnlineTopUp |
| ? data.min_topup |
| : enableStripeTopUp |
| ? data.stripe_min_topup |
| : 1; |
| setEnableOnlineTopUp(enableOnlineTopUp); |
| setEnableStripeTopUp(enableStripeTopUp); |
| setEnableCreemTopUp(enableCreemTopUp); |
| setMinTopUp(minTopUpValue); |
| setTopUpCount(minTopUpValue); |
|
|
| |
| try { |
| console.log(' data is ?', data); |
| console.log(' creem products is ?', data.creem_products); |
| const products = JSON.parse(data.creem_products || '[]'); |
| setCreemProducts(products); |
| } catch (e) { |
| setCreemProducts([]); |
| } |
|
|
| |
| if (topupInfo.amount_options.length === 0) { |
| setPresetAmounts(generatePresetAmounts(minTopUpValue)); |
| } |
|
|
| |
| getAmount(minTopUpValue); |
| } catch (e) { |
| console.log('解析支付方式失败:', e); |
| setPayMethods([]); |
| } |
|
|
| |
| if (data.amount_options && data.amount_options.length > 0) { |
| const customPresets = data.amount_options.map((amount) => ({ |
| value: amount, |
| discount: data.discount[amount] || 1.0, |
| })); |
| setPresetAmounts(customPresets); |
| } |
| } else { |
| console.error('获取充值配置失败:', data); |
| } |
| } catch (error) { |
| console.error('获取充值配置异常:', error); |
| } |
| }; |
|
|
| |
| const getAffLink = async () => { |
| const res = await API.get('/api/user/aff'); |
| const { success, message, data } = res.data; |
| if (success) { |
| let link = `${window.location.origin}/register?aff=${data}`; |
| setAffLink(link); |
| } else { |
| showError(message); |
| } |
| }; |
|
|
| |
| const transfer = async () => { |
| if (transferAmount < getQuotaPerUnit()) { |
| showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit())); |
| return; |
| } |
| const res = await API.post(`/api/user/aff_transfer`, { |
| quota: transferAmount, |
| }); |
| const { success, message } = res.data; |
| if (success) { |
| showSuccess(message); |
| setOpenTransfer(false); |
| getUserQuota().then(); |
| } else { |
| showError(message); |
| } |
| }; |
|
|
| |
| const handleAffLinkClick = async () => { |
| await copy(affLink); |
| showSuccess(t('邀请链接已复制到剪切板')); |
| }; |
|
|
| useEffect(() => { |
| if (!userState?.user?.id) { |
| getUserQuota().then(); |
| } |
| setTransferAmount(getQuotaPerUnit()); |
| }, []); |
|
|
| useEffect(() => { |
| if (affFetchedRef.current) return; |
| affFetchedRef.current = true; |
| getAffLink().then(); |
| }, []); |
|
|
| |
| useEffect(() => { |
| getTopupInfo().then(); |
| }, []); |
|
|
| useEffect(() => { |
| if (statusState?.status) { |
| |
| |
| |
| setTopUpLink(statusState.status.top_up_link || ''); |
| setPriceRatio(statusState.status.price || 1); |
|
|
| setStatusLoading(false); |
| } |
| }, [statusState?.status]); |
|
|
| const renderAmount = () => { |
| return amount + ' ' + t('元'); |
| }; |
|
|
| const getAmount = async (value) => { |
| if (value === undefined) { |
| value = topUpCount; |
| } |
| setAmountLoading(true); |
| try { |
| const res = await API.post('/api/user/amount', { |
| amount: parseFloat(value), |
| }); |
| if (res !== undefined) { |
| const { message, data } = res.data; |
| if (message === 'success') { |
| setAmount(parseFloat(data)); |
| } else { |
| setAmount(0); |
| Toast.error({ content: '错误:' + data, id: 'getAmount' }); |
| } |
| } else { |
| showError(res); |
| } |
| } catch (err) { |
| console.log(err); |
| } |
| setAmountLoading(false); |
| }; |
|
|
| const getStripeAmount = async (value) => { |
| if (value === undefined) { |
| value = topUpCount; |
| } |
| setAmountLoading(true); |
| try { |
| const res = await API.post('/api/user/stripe/amount', { |
| amount: parseFloat(value), |
| }); |
| if (res !== undefined) { |
| const { message, data } = res.data; |
| if (message === 'success') { |
| setAmount(parseFloat(data)); |
| } else { |
| setAmount(0); |
| Toast.error({ content: '错误:' + data, id: 'getAmount' }); |
| } |
| } else { |
| showError(res); |
| } |
| } catch (err) { |
| console.log(err); |
| } finally { |
| setAmountLoading(false); |
| } |
| }; |
|
|
| const handleCancel = () => { |
| setOpen(false); |
| }; |
|
|
| const handleTransferCancel = () => { |
| setOpenTransfer(false); |
| }; |
|
|
| const handleOpenHistory = () => { |
| setOpenHistory(true); |
| }; |
|
|
| const handleHistoryCancel = () => { |
| setOpenHistory(false); |
| }; |
|
|
| const handleCreemCancel = () => { |
| setCreemOpen(false); |
| setSelectedCreemProduct(null); |
| }; |
|
|
| |
| const selectPresetAmount = (preset) => { |
| setTopUpCount(preset.value); |
| setSelectedPreset(preset.value); |
|
|
| |
| const discount = preset.discount || topupInfo.discount[preset.value] || 1.0; |
| const discountedAmount = preset.value * priceRatio * discount; |
| setAmount(discountedAmount); |
| }; |
|
|
| |
| const formatLargeNumber = (num) => { |
| return num.toString(); |
| }; |
|
|
| |
| const generatePresetAmounts = (minAmount) => { |
| const multipliers = [1, 5, 10, 30, 50, 100, 300, 500]; |
| return multipliers.map((multiplier) => ({ |
| value: minAmount * multiplier, |
| })); |
| }; |
|
|
| return ( |
| <div className='w-full max-w-7xl mx-auto relative min-h-screen lg:min-h-0 mt-[60px] px-2'> |
| {/* 划转模态框 */} |
| <TransferModal |
| t={t} |
| openTransfer={openTransfer} |
| transfer={transfer} |
| handleTransferCancel={handleTransferCancel} |
| userState={userState} |
| renderQuota={renderQuota} |
| getQuotaPerUnit={getQuotaPerUnit} |
| transferAmount={transferAmount} |
| setTransferAmount={setTransferAmount} |
| /> |
| |
| {/* 充值确认模态框 */} |
| <PaymentConfirmModal |
| t={t} |
| open={open} |
| onlineTopUp={onlineTopUp} |
| handleCancel={handleCancel} |
| confirmLoading={confirmLoading} |
| topUpCount={topUpCount} |
| renderQuotaWithAmount={renderQuotaWithAmount} |
| amountLoading={amountLoading} |
| renderAmount={renderAmount} |
| payWay={payWay} |
| payMethods={payMethods} |
| amountNumber={amount} |
| discountRate={topupInfo?.discount?.[topUpCount] || 1.0} |
| /> |
| |
| {/* 充值账单模态框 */} |
| <TopupHistoryModal |
| visible={openHistory} |
| onCancel={handleHistoryCancel} |
| t={t} |
| /> |
| |
| {/* Creem 充值确认模态框 */} |
| <Modal |
| title={t('确定要充值 $')} |
| visible={creemOpen} |
| onOk={onlineCreemTopUp} |
| onCancel={handleCreemCancel} |
| maskClosable={false} |
| size='small' |
| centered |
| confirmLoading={confirmLoading} |
| > |
| {selectedCreemProduct && ( |
| <> |
| <p> |
| {t('产品名称')}:{selectedCreemProduct.name} |
| </p> |
| <p> |
| {t('价格')}:{selectedCreemProduct.currency === 'EUR' ? '€' : '$'}{selectedCreemProduct.price} |
| </p> |
| <p> |
| {t('充值额度')}:{selectedCreemProduct.quota} |
| </p> |
| <p>{t('是否确认充值?')}</p> |
| </> |
| )} |
| </Modal> |
|
|
| {} |
| <div className='space-y-6'> |
| <div className='grid grid-cols-1 lg:grid-cols-12 gap-6'> |
| {/* 左侧充值区域 */} |
| <div className='lg:col-span-7 space-y-6 w-full'> |
| <RechargeCard |
| t={t} |
| enableOnlineTopUp={enableOnlineTopUp} |
| enableStripeTopUp={enableStripeTopUp} |
| enableCreemTopUp={enableCreemTopUp} |
| creemProducts={creemProducts} |
| creemPreTopUp={creemPreTopUp} |
| presetAmounts={presetAmounts} |
| selectedPreset={selectedPreset} |
| selectPresetAmount={selectPresetAmount} |
| formatLargeNumber={formatLargeNumber} |
| priceRatio={priceRatio} |
| topUpCount={topUpCount} |
| minTopUp={minTopUp} |
| renderQuotaWithAmount={renderQuotaWithAmount} |
| getAmount={getAmount} |
| setTopUpCount={setTopUpCount} |
| setSelectedPreset={setSelectedPreset} |
| renderAmount={renderAmount} |
| amountLoading={amountLoading} |
| payMethods={payMethods} |
| preTopUp={preTopUp} |
| paymentLoading={paymentLoading} |
| payWay={payWay} |
| redemptionCode={redemptionCode} |
| setRedemptionCode={setRedemptionCode} |
| topUp={topUp} |
| isSubmitting={isSubmitting} |
| topUpLink={topUpLink} |
| openTopUpLink={openTopUpLink} |
| userState={userState} |
| renderQuota={renderQuota} |
| statusLoading={statusLoading} |
| topupInfo={topupInfo} |
| onOpenHistory={handleOpenHistory} |
| /> |
| </div> |
| |
| {/* 右侧信息区域 */} |
| <div className='lg:col-span-5'> |
| <InvitationCard |
| t={t} |
| userState={userState} |
| renderQuota={renderQuota} |
| setOpenTransfer={setOpenTransfer} |
| affLink={affLink} |
| handleAffLinkClick={handleAffLinkClick} |
| /> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default TopUp; |
|
|