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) => (
+
+
+ }
+ onClick={() => deleteProduct(record.productId)}
+ />
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ Creem 是一个简单的支付处理平台,支持固定金额的产品销售。请在
+
+ Creem 官网
+
+ 创建账户并获取 API 密钥。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('产品配置')}
+ }
+ onClick={() => openProductModal()}
+ >
+ {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 && (