diff --git a/web/src/components/settings/PaymentSetting.js b/web/src/components/settings/PaymentSetting.js index ed175a20..5d3677fc 100644 --- a/web/src/components/settings/PaymentSetting.js +++ b/web/src/components/settings/PaymentSetting.js @@ -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 = () => { + + + ); diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js b/web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js new file mode 100644 index 00000000..2e58b86d --- /dev/null +++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js @@ -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) => ( +
+ +
+ ), + }, + ]; + + return ( + +
(formApiRef.current = api)} + > + + + Creem 是一个简单的支付处理平台,支持固定金额的产品销售。请在 + + Creem 官网 + + 创建账户并获取 API 密钥。 +
+
+ + + + + + + + + + + +
+
+ {t('产品配置')} + +
+ + + {t('暂无产品配置')} + + } + /> + + + + + + + {/* 产品配置模态框 */} + +
+
+ + {t('产品名称')} + + setProductForm({ ...productForm, name: value })} + placeholder={t('例如:基础套餐')} + size='large' + /> +
+
+ + {t('产品ID')} + + setProductForm({ ...productForm, productId: value })} + placeholder={t('例如:prod_6I8rBerHpPxyoiU9WK4kot')} + size='large' + disabled={!!editingProduct} + /> +
+
+ + {t('货币')} + + +
+
+ + {t('价格')} ({productForm.currency === 'EUR' ? '欧元' : '美元'}) + + setProductForm({ ...productForm, price: value })} + placeholder={t('例如:4.99')} + min={0.01} + precision={2} + size='large' + className='w-full' + /> +
+
+ + {t('充值额度')} + + setProductForm({ ...productForm, quota: value })} + placeholder={t('例如:100000')} + min={1} + precision={0} + size='large' + className='w-full' + /> +
+
+
+ + ); +} \ No newline at end of file diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index a7ac6ba6..d7625685 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -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 = () => {

{t('是否确认充值?')}

+ + {selectedCreemProduct && ( + <> +

+ {t('产品名称')}:{selectedCreemProduct.name} +

+

+ {t('价格')}:{selectedCreemProduct.currency === 'EUR' ? '€' : '$'}{selectedCreemProduct.price} +

+

+ {t('充值额度')}:{selectedCreemProduct.quota} +

+

{t('是否确认充值?')}

+ + )} +
+
{/* 左侧充值区域 */}
@@ -925,7 +1014,7 @@ const TopUp = () => { )} - {!enableOnlineTopUp && !enableStripeTopUp && ( + {!enableOnlineTopUp && !enableStripeTopUp && !enableCreemTopUp && ( {
- + + {/* 移动端 Stripe 充值区域 */} +
+ + + {t('Stripe 充值')} + + + +
+
+ {t('充值数量')} + {amountLoading ? ( + + ) : ( + + {t('实付金额:') + renderStripeAmount()} + + )} +
+ { + 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 + } + /> +
+ +
+ +
+
+ + )} + + {enableCreemTopUp && creemProducts.length > 0 && ( + <> +
+ + + {t('Creem 充值')} + + + +
+ + {t('选择充值套餐')} + +
+ {creemProducts.map((product, index) => ( + creemPreTopUp(product)} + className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300' + bodyStyle={{ textAlign: 'center', padding: '16px' }} + > +
+ {product.name} +
+
+ {t('充值额度')}: {product.quota} +
+
+ {product.currency === 'EUR' ? '€' : '$'}{product.price} +
+
+ ))} +
+
+
+ + {/* 移动端 Creem 充值区域 */} +
+ + + {t('Creem 充值')} + + + +
+ + {t('选择充值套餐')} + +
+ {creemProducts.map((product, index) => ( + creemPreTopUp(product)} + className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300' + bodyStyle={{ textAlign: 'center', padding: '16px' }} + > +
+ {product.name} +
+
+ {t('充值额度')}: {product.quota} +
+
+ {product.currency === 'EUR' ? '€' : '$'}{product.price} +
+
+ ))} +
+
+
+ )} @@ -1185,7 +1418,12 @@ const TopUp = () => { - {/* 移动端底部固定的自定义金额和支付区域 */} + {/* 移动端底部间距,避免内容被固定区域遮挡 */} + {enableOnlineTopUp && ( +
+ )} + + {/* 移动端底部固定的自定义金额和支付区域 - 仅限在线充值 */} {enableOnlineTopUp && (