前端部分,调试 完善
This commit is contained in:
parent
51a7aa440b
commit
edf46c701f
@ -3,9 +3,11 @@ import { Card, Spin } from '@douyinfe/semi-ui';
|
|||||||
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
|
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
|
||||||
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
|
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
|
||||||
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe.js';
|
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe.js';
|
||||||
|
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem.js';
|
||||||
import { API, showError, toBoolean } from '../../helpers';
|
import { API, showError, toBoolean } from '../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
const PaymentSetting = () => {
|
const PaymentSetting = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
@ -24,6 +26,9 @@ const PaymentSetting = () => {
|
|||||||
StripePriceId: '',
|
StripePriceId: '',
|
||||||
StripeUnitPrice: 8.0,
|
StripeUnitPrice: 8.0,
|
||||||
StripeMinTopUp: 1,
|
StripeMinTopUp: 1,
|
||||||
|
|
||||||
|
CreemApiKey: '',
|
||||||
|
CreemProducts: '[]',
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@ -43,6 +48,14 @@ const PaymentSetting = () => {
|
|||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'CreemProducts':
|
||||||
|
try {
|
||||||
|
newInputs[item.key] = item.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析CreemProducts出错:', error);
|
||||||
|
newInputs[item.key] = '[]';
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'Price':
|
case 'Price':
|
||||||
case 'MinTopUp':
|
case 'MinTopUp':
|
||||||
case 'StripeUnitPrice':
|
case 'StripeUnitPrice':
|
||||||
@ -92,6 +105,9 @@ const PaymentSetting = () => {
|
|||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsPaymentGatewayStripe options={inputs} refresh={onRefresh} />
|
<SettingsPaymentGatewayStripe options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card style={{ marginTop: '10px' }}>
|
||||||
|
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
|
||||||
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
373
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js
Normal file
373
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Banner,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Typography,
|
||||||
|
Spin,
|
||||||
|
Table,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
const { Text } = Typography;
|
||||||
|
import {
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
} from '../../../helpers';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function SettingsPaymentGatewayCreem(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
CreemApiKey: '',
|
||||||
|
CreemProducts: '[]',
|
||||||
|
CreemTestMode: false,
|
||||||
|
});
|
||||||
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [showProductModal, setShowProductModal] = useState(false);
|
||||||
|
const [editingProduct, setEditingProduct] = useState(null);
|
||||||
|
const [productForm, setProductForm] = useState({
|
||||||
|
name: '',
|
||||||
|
productId: '',
|
||||||
|
price: 0,
|
||||||
|
quota: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
const formApiRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.options && formApiRef.current) {
|
||||||
|
const currentInputs = {
|
||||||
|
CreemApiKey: props.options.CreemApiKey || '',
|
||||||
|
CreemProducts: props.options.CreemProducts || '[]',
|
||||||
|
CreemTestMode: props.options.CreemTestMode === 'true',
|
||||||
|
};
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setOriginInputs({ ...currentInputs });
|
||||||
|
formApiRef.current.setValues(currentInputs);
|
||||||
|
|
||||||
|
// Parse products
|
||||||
|
try {
|
||||||
|
const parsedProducts = JSON.parse(currentInputs.CreemProducts);
|
||||||
|
setProducts(parsedProducts);
|
||||||
|
} catch (e) {
|
||||||
|
setProducts([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.options]);
|
||||||
|
|
||||||
|
const handleFormChange = (values) => {
|
||||||
|
setInputs(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitCreemSetting = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
if (inputs.CreemApiKey && inputs.CreemApiKey !== '') {
|
||||||
|
options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save test mode setting
|
||||||
|
options.push({ key: 'CreemTestMode', value: inputs.CreemTestMode ? 'true' : 'false' });
|
||||||
|
|
||||||
|
// Save products as JSON string
|
||||||
|
options.push({ key: 'CreemProducts', value: JSON.stringify(products) });
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const requestQueue = options.map(opt =>
|
||||||
|
API.put('/api/option/', {
|
||||||
|
key: opt.key,
|
||||||
|
value: opt.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(requestQueue);
|
||||||
|
|
||||||
|
// 检查所有请求是否成功
|
||||||
|
const errorResults = results.filter(res => !res.data.success);
|
||||||
|
if (errorResults.length > 0) {
|
||||||
|
errorResults.forEach(res => {
|
||||||
|
showError(res.data.message);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showSuccess(t('更新成功'));
|
||||||
|
// 更新本地存储的原始值
|
||||||
|
setOriginInputs({ ...inputs });
|
||||||
|
props.refresh?.();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('更新失败'));
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProductModal = (product = null) => {
|
||||||
|
if (product) {
|
||||||
|
setEditingProduct(product);
|
||||||
|
setProductForm({ ...product });
|
||||||
|
} else {
|
||||||
|
setEditingProduct(null);
|
||||||
|
setProductForm({
|
||||||
|
name: '',
|
||||||
|
productId: '',
|
||||||
|
price: 0,
|
||||||
|
quota: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShowProductModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeProductModal = () => {
|
||||||
|
setShowProductModal(false);
|
||||||
|
setEditingProduct(null);
|
||||||
|
setProductForm({
|
||||||
|
name: '',
|
||||||
|
productId: '',
|
||||||
|
price: 0,
|
||||||
|
quota: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveProduct = () => {
|
||||||
|
if (!productForm.name || !productForm.productId || productForm.price <= 0 || productForm.quota <= 0 || !productForm.currency) {
|
||||||
|
showError(t('请填写完整的产品信息'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newProducts = [...products];
|
||||||
|
if (editingProduct) {
|
||||||
|
// 编辑现有产品
|
||||||
|
const index = newProducts.findIndex(p => p.productId === editingProduct.productId);
|
||||||
|
if (index !== -1) {
|
||||||
|
newProducts[index] = { ...productForm };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 添加新产品
|
||||||
|
if (newProducts.find(p => p.productId === productForm.productId)) {
|
||||||
|
showError(t('产品ID已存在'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newProducts.push({ ...productForm });
|
||||||
|
}
|
||||||
|
|
||||||
|
setProducts(newProducts);
|
||||||
|
closeProductModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProduct = (productId) => {
|
||||||
|
const newProducts = products.filter(p => p.productId !== productId);
|
||||||
|
setProducts(newProducts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('产品名称'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('产品ID'),
|
||||||
|
dataIndex: 'productId',
|
||||||
|
key: 'productId',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('价格'),
|
||||||
|
dataIndex: 'price',
|
||||||
|
key: 'price',
|
||||||
|
render: (price, record) => `${record.currency === 'EUR' ? '€' : '$'}${price}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('充值额度'),
|
||||||
|
dataIndex: 'quota',
|
||||||
|
key: 'quota',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('操作'),
|
||||||
|
key: 'action',
|
||||||
|
render: (_, record) => (
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
onClick={() => openProductModal(record)}
|
||||||
|
>
|
||||||
|
{t('编辑')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='danger'
|
||||||
|
theme='borderless'
|
||||||
|
size='small'
|
||||||
|
icon={<Trash2 size={14} />}
|
||||||
|
onClick={() => deleteProduct(record.productId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
initValues={inputs}
|
||||||
|
onValueChange={handleFormChange}
|
||||||
|
getFormApi={(api) => (formApiRef.current = api)}
|
||||||
|
>
|
||||||
|
<Form.Section text={t('Creem 设置')}>
|
||||||
|
<Text>
|
||||||
|
Creem 是一个简单的支付处理平台,支持固定金额的产品销售。请在
|
||||||
|
<a
|
||||||
|
href='https://creem.io'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
Creem 官网
|
||||||
|
</a>
|
||||||
|
创建账户并获取 API 密钥。
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
<Banner
|
||||||
|
type='info'
|
||||||
|
description={t('Creem 只支持预设的固定金额产品,不支持自定义金额充值')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||||
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||||
|
<Form.Input
|
||||||
|
field='CreemApiKey'
|
||||||
|
label={t('API 密钥')}
|
||||||
|
placeholder={t('creem_xxx 的 Creem API 密钥,敏感信息不显示')}
|
||||||
|
type='password'
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||||
|
<Form.Switch
|
||||||
|
field='CreemTestMode'
|
||||||
|
label={t('测试模式')}
|
||||||
|
extraText={t('启用后将使用 Creem 测试环境,可使用测试卡号 4242 4242 4242 4242 进行测试')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<div className='flex justify-between items-center mb-4'>
|
||||||
|
<Text strong>{t('产品配置')}</Text>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<Plus size={16} />}
|
||||||
|
onClick={() => openProductModal()}
|
||||||
|
>
|
||||||
|
{t('添加产品')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={products}
|
||||||
|
pagination={false}
|
||||||
|
empty={
|
||||||
|
<div className='text-center py-8'>
|
||||||
|
<Text type='tertiary'>{t('暂无产品配置')}</Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={submitCreemSetting} style={{ marginTop: 16 }}>
|
||||||
|
{t('更新 Creem 设置')}
|
||||||
|
</Button>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* 产品配置模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={editingProduct ? t('编辑产品') : t('添加产品')}
|
||||||
|
visible={showProductModal}
|
||||||
|
onOk={saveProduct}
|
||||||
|
onCancel={closeProductModal}
|
||||||
|
maskClosable={false}
|
||||||
|
size='small'
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-2'>
|
||||||
|
{t('产品名称')}
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={productForm.name}
|
||||||
|
onChange={(value) => setProductForm({ ...productForm, name: value })}
|
||||||
|
placeholder={t('例如:基础套餐')}
|
||||||
|
size='large'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-2'>
|
||||||
|
{t('产品ID')}
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={productForm.productId}
|
||||||
|
onChange={(value) => setProductForm({ ...productForm, productId: value })}
|
||||||
|
placeholder={t('例如:prod_6I8rBerHpPxyoiU9WK4kot')}
|
||||||
|
size='large'
|
||||||
|
disabled={!!editingProduct}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-2'>
|
||||||
|
{t('货币')}
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={productForm.currency}
|
||||||
|
onChange={(value) => setProductForm({ ...productForm, currency: value })}
|
||||||
|
size='large'
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
<Select.Option value='USD'>USD (美元)</Select.Option>
|
||||||
|
<Select.Option value='EUR'>EUR (欧元)</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-2'>
|
||||||
|
{t('价格')} ({productForm.currency === 'EUR' ? '欧元' : '美元'})
|
||||||
|
</Text>
|
||||||
|
<InputNumber
|
||||||
|
value={productForm.price}
|
||||||
|
onChange={(value) => setProductForm({ ...productForm, price: value })}
|
||||||
|
placeholder={t('例如:4.99')}
|
||||||
|
min={0.01}
|
||||||
|
precision={2}
|
||||||
|
size='large'
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-2'>
|
||||||
|
{t('充值额度')}
|
||||||
|
</Text>
|
||||||
|
<InputNumber
|
||||||
|
value={productForm.quota}
|
||||||
|
onChange={(value) => setProductForm({ ...productForm, quota: value })}
|
||||||
|
placeholder={t('例如:100000')}
|
||||||
|
min={1}
|
||||||
|
precision={0}
|
||||||
|
size='large'
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -66,6 +66,11 @@ const TopUp = () => {
|
|||||||
const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
|
const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
|
||||||
const [stripeOpen, setStripeOpen] = useState(false);
|
const [stripeOpen, setStripeOpen] = useState(false);
|
||||||
|
|
||||||
|
const [creemProducts, setCreemProducts] = useState([]);
|
||||||
|
const [enableCreemTopUp, setEnableCreemTopUp] = useState(false);
|
||||||
|
const [creemOpen, setCreemOpen] = useState(false);
|
||||||
|
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
|
||||||
|
|
||||||
const [userQuota, setUserQuota] = useState(0);
|
const [userQuota, setUserQuota] = useState(0);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -296,6 +301,50 @@ const TopUp = () => {
|
|||||||
window.open(data.pay_link, '_blank');
|
window.open(data.pay_link, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const creemPreTopUp = async (product) => {
|
||||||
|
if (!enableCreemTopUp) {
|
||||||
|
showError(t('管理员未开启 Creem 充值!'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedCreemProduct(product);
|
||||||
|
setCreemOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onlineCreemTopUp = async () => {
|
||||||
|
if (!selectedCreemProduct) {
|
||||||
|
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) => {
|
||||||
|
// 与 Stripe 保持一致的实现方式
|
||||||
|
window.open(data.checkout_url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
const getUserQuota = async () => {
|
const getUserQuota = async () => {
|
||||||
setUserDataLoading(true);
|
setUserDataLoading(true);
|
||||||
let res = await API.get(`/api/user/self`);
|
let res = await API.get(`/api/user/self`);
|
||||||
@ -396,6 +445,15 @@ const TopUp = () => {
|
|||||||
setStripeMinTopUp(statusState.status.stripe_min_topup || 1);
|
setStripeMinTopUp(statusState.status.stripe_min_topup || 1);
|
||||||
setStripeTopUpCount(statusState.status.stripe_min_topup || 1);
|
setStripeTopUpCount(statusState.status.stripe_min_topup || 1);
|
||||||
setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
|
setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
|
||||||
|
|
||||||
|
// Creem settings
|
||||||
|
setEnableCreemTopUp(statusState.status.enable_creem_topup || false);
|
||||||
|
try {
|
||||||
|
const products = JSON.parse(statusState.status.creem_products || '[]');
|
||||||
|
setCreemProducts(products);
|
||||||
|
} catch (e) {
|
||||||
|
setCreemProducts([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [statusState?.status]);
|
}, [statusState?.status]);
|
||||||
|
|
||||||
@ -470,6 +528,11 @@ const TopUp = () => {
|
|||||||
setStripeOpen(false);
|
setStripeOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreemCancel = () => {
|
||||||
|
setCreemOpen(false);
|
||||||
|
setSelectedCreemProduct(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTransferCancel = () => {
|
const handleTransferCancel = () => {
|
||||||
setOpenTransfer(false);
|
setOpenTransfer(false);
|
||||||
};
|
};
|
||||||
@ -623,6 +686,32 @@ const TopUp = () => {
|
|||||||
<p>{t('是否确认充值?')}</p>
|
<p>{t('是否确认充值?')}</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<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='grid grid-cols-1 lg:grid-cols-12 gap-6'>
|
<div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
|
||||||
{/* 左侧充值区域 */}
|
{/* 左侧充值区域 */}
|
||||||
<div className='lg:col-span-7 space-y-6 w-full'>
|
<div className='lg:col-span-7 space-y-6 w-full'>
|
||||||
@ -925,7 +1014,7 @@ const TopUp = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!enableOnlineTopUp && !enableStripeTopUp && (
|
{!enableOnlineTopUp && !enableStripeTopUp && !enableCreemTopUp && (
|
||||||
<Banner
|
<Banner
|
||||||
type='warning'
|
type='warning'
|
||||||
description={t(
|
description={t(
|
||||||
@ -1016,7 +1105,151 @@ const TopUp = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{/* 移动端 Stripe 充值区域 */}
|
||||||
|
<div className='md:hidden space-y-4'>
|
||||||
|
<Divider style={{ margin: '24px 0' }}>
|
||||||
|
<Text className='text-sm font-medium'>
|
||||||
|
{t('Stripe 充值')}
|
||||||
|
</Text>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className='flex justify-between mb-2'>
|
||||||
|
<Text strong>{t('充值数量')}</Text>
|
||||||
|
{amountLoading ? (
|
||||||
|
<Skeleton.Title
|
||||||
|
style={{ width: '80px', height: '16px' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text type='tertiary'>
|
||||||
|
{t('实付金额:') + renderStripeAmount()}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<InputNumber
|
||||||
|
disabled={!enableStripeTopUp}
|
||||||
|
placeholder={
|
||||||
|
t('充值数量,最低 ') + renderQuotaWithAmount(stripeMinTopUp)
|
||||||
|
}
|
||||||
|
value={stripeTopUpCount}
|
||||||
|
min={stripeMinTopUp}
|
||||||
|
max={999999999}
|
||||||
|
step={1}
|
||||||
|
precision={0}
|
||||||
|
onChange={async (value) => {
|
||||||
|
if (value && value >= 1) {
|
||||||
|
setStripeTopUpCount(value);
|
||||||
|
setSelectedPreset(null);
|
||||||
|
await getStripeAmount(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
if (!value || value < 1) {
|
||||||
|
setStripeTopUpCount(1);
|
||||||
|
getStripeAmount(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='w-full'
|
||||||
|
formatter={(value) => (value ? `${value}` : '')}
|
||||||
|
parser={(value) =>
|
||||||
|
value ? parseInt(value.replace(/[^\d]/g, '')) : 0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => stripePreTopUp()}
|
||||||
|
disabled={!enableStripeTopUp}
|
||||||
|
loading={paymentLoading && payWay === 'stripe'}
|
||||||
|
icon={<CreditCard size={16} />}
|
||||||
|
style={{
|
||||||
|
height: '40px',
|
||||||
|
color: '#b161fe',
|
||||||
|
}}
|
||||||
|
className='transition-all hover:shadow-md w-full'
|
||||||
|
>
|
||||||
|
<span className='ml-1'>Stripe</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableCreemTopUp && creemProducts.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className='hidden md:block space-y-4'>
|
||||||
|
<Divider style={{ margin: '24px 0' }}>
|
||||||
|
<Text className='text-sm font-medium'>
|
||||||
|
{t('Creem 充值')}
|
||||||
|
</Text>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-3'>
|
||||||
|
{t('选择充值套餐')}
|
||||||
|
</Text>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端 Creem 充值区域 */}
|
||||||
|
<div className='md:hidden space-y-4'>
|
||||||
|
<Divider style={{ margin: '24px 0' }}>
|
||||||
|
<Text className='text-sm font-medium'>
|
||||||
|
{t('Creem 充值')}
|
||||||
|
</Text>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong className='block mb-3'>
|
||||||
|
{t('选择充值套餐')}
|
||||||
|
</Text>
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider style={{ margin: '24px 0' }}>
|
<Divider style={{ margin: '24px 0' }}>
|
||||||
@ -1185,7 +1418,12 @@ const TopUp = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端底部固定的自定义金额和支付区域 */}
|
{/* 移动端底部间距,避免内容被固定区域遮挡 */}
|
||||||
|
{enableOnlineTopUp && (
|
||||||
|
<div className='md:hidden h-32'></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 移动端底部固定的自定义金额和支付区域 - 仅限在线充值 */}
|
||||||
{enableOnlineTopUp && (
|
{enableOnlineTopUp && (
|
||||||
<div
|
<div
|
||||||
className='md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50'
|
className='md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user