前端部分,调试 完善
This commit is contained in:
@@ -3,9 +3,11 @@ import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
|
||||
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
|
||||
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe.js';
|
||||
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem.js';
|
||||
import { API, showError, toBoolean } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
const PaymentSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
@@ -24,6 +26,9 @@ const PaymentSetting = () => {
|
||||
StripePriceId: '',
|
||||
StripeUnitPrice: 8.0,
|
||||
StripeMinTopUp: 1,
|
||||
|
||||
CreemApiKey: '',
|
||||
CreemProducts: '[]',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -43,6 +48,14 @@ const PaymentSetting = () => {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
break;
|
||||
case 'CreemProducts':
|
||||
try {
|
||||
newInputs[item.key] = item.value;
|
||||
} catch (error) {
|
||||
console.error('解析CreemProducts出错:', error);
|
||||
newInputs[item.key] = '[]';
|
||||
}
|
||||
break;
|
||||
case 'Price':
|
||||
case 'MinTopUp':
|
||||
case 'StripeUnitPrice':
|
||||
@@ -92,6 +105,9 @@ const PaymentSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayStripe options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</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 [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 [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -296,6 +301,50 @@ const TopUp = () => {
|
||||
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 () => {
|
||||
setUserDataLoading(true);
|
||||
let res = await API.get(`/api/user/self`);
|
||||
@@ -396,6 +445,15 @@ const TopUp = () => {
|
||||
setStripeMinTopUp(statusState.status.stripe_min_topup || 1);
|
||||
setStripeTopUpCount(statusState.status.stripe_min_topup || 1);
|
||||
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]);
|
||||
|
||||
@@ -470,6 +528,11 @@ const TopUp = () => {
|
||||
setStripeOpen(false);
|
||||
};
|
||||
|
||||
const handleCreemCancel = () => {
|
||||
setCreemOpen(false);
|
||||
setSelectedCreemProduct(null);
|
||||
};
|
||||
|
||||
const handleTransferCancel = () => {
|
||||
setOpenTransfer(false);
|
||||
};
|
||||
@@ -623,6 +686,32 @@ const TopUp = () => {
|
||||
<p>{t('是否确认充值?')}</p>
|
||||
</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='lg:col-span-7 space-y-6 w-full'>
|
||||
@@ -925,7 +1014,7 @@ const TopUp = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{!enableOnlineTopUp && !enableStripeTopUp && (
|
||||
{!enableOnlineTopUp && !enableStripeTopUp && !enableCreemTopUp && (
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t(
|
||||
@@ -1016,7 +1105,151 @@ const TopUp = () => {
|
||||
</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' }}>
|
||||
@@ -1185,7 +1418,12 @@ const TopUp = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端底部固定的自定义金额和支付区域 */}
|
||||
{/* 移动端底部间距,避免内容被固定区域遮挡 */}
|
||||
{enableOnlineTopUp && (
|
||||
<div className='md:hidden h-32'></div>
|
||||
)}
|
||||
|
||||
{/* 移动端底部固定的自定义金额和支付区域 - 仅限在线充值 */}
|
||||
{enableOnlineTopUp && (
|
||||
<div
|
||||
className='md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50'
|
||||
|
||||
Reference in New Issue
Block a user