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