Merge pull request #4089 from seefs001/feature/waffo-pay

rafactor: payment
This commit is contained in:
Seefs
2026-04-18 14:22:54 +08:00
committed by GitHub
parent 5b9dcf1bda
commit f995a868e4
41 changed files with 3222 additions and 740 deletions
+75 -15
View File
@@ -18,12 +18,13 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment';
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
import SettingsPaymentGatewayWaffoPancake from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake';
import { API, showError, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next';
@@ -48,6 +49,17 @@ const PaymentSetting = () => {
StripeUnitPrice: 8.0,
StripeMinTopUp: 1,
StripePromotionCodesEnabled: false,
WaffoPancakeEnabled: false,
WaffoPancakeSandbox: false,
WaffoPancakeMerchantID: '',
WaffoPancakePrivateKey: '',
WaffoPancakeStoreID: '',
WaffoPancakeProductID: '',
WaffoPancakeReturnURL: '',
WaffoPancakeCurrency: 'USD',
WaffoPancakeUnitPrice: 1.0,
WaffoPancakeMinTopUp: 1,
});
let [loading, setLoading] = useState(false);
@@ -96,8 +108,21 @@ const PaymentSetting = () => {
case 'MinTopUp':
case 'StripeUnitPrice':
case 'StripeMinTopUp':
case 'WaffoPancakeUnitPrice':
case 'WaffoPancakeMinTopUp':
newInputs[item.key] = parseFloat(item.value);
break;
case 'WaffoPancakeMerchantID':
case 'WaffoPancakePrivateKey':
case 'WaffoPancakeStoreID':
case 'WaffoPancakeProductID':
case 'WaffoPancakeReturnURL':
case 'WaffoPancakeCurrency':
newInputs[item.key] = item.value;
break;
case 'WaffoPancakeSandbox':
newInputs[item.key] = toBoolean(item.value);
break;
default:
if (item.key.endsWith('Enabled')) {
newInputs[item.key] = toBoolean(item.value);
@@ -108,7 +133,7 @@ const PaymentSetting = () => {
}
});
setInputs(newInputs);
setInputs((prev) => ({ ...prev, ...newInputs }));
} else {
showError(t(message));
}
@@ -133,19 +158,54 @@ const PaymentSetting = () => {
<>
<Spin spinning={loading} size='large'>
<Card style={{ marginTop: '10px' }}>
<SettingsGeneralPayment options={inputs} refresh={onRefresh} />
</Card>
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGateway options={inputs} refresh={onRefresh} />
</Card>
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayStripe options={inputs} refresh={onRefresh} />
</Card>
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
</Card>
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayWaffo options={inputs} refresh={onRefresh} />
<Tabs
type='card'
defaultActiveKey='general'
contentStyle={{ paddingTop: 24 }}
>
<Tabs.TabPane tab={t('通用设置')} itemKey='general'>
<SettingsGeneralPayment
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('易支付设置')} itemKey='epay'>
<SettingsPaymentGateway
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Stripe 设置')} itemKey='stripe'>
<SettingsPaymentGatewayStripe
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Creem 设置')} itemKey='creem'>
<SettingsPaymentGatewayCreem
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Waffo 设置')} itemKey='waffo'>
<SettingsPaymentGatewayWaffo
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
{/*<Tabs.TabPane tab={t('Waffo Pancake 设置')} itemKey='waffo-pancake'>*/}
{/* <SettingsPaymentGatewayWaffoPancake*/}
{/* options={inputs}*/}
{/* refresh={onRefresh}*/}
{/* hideSectionTitle*/}
{/* />*/}
{/*</Tabs.TabPane>*/}
</Tabs>
</Card>
</Spin>
</>
+59 -57
View File
@@ -21,7 +21,6 @@ import React, { useEffect, useRef, useState } from 'react';
import {
Avatar,
Typography,
Tag,
Card,
Button,
Banner,
@@ -88,8 +87,7 @@ const RechargeCard = ({
topupInfo,
onOpenHistory,
enableWaffoTopUp,
waffoTopUp,
waffoPayMethods,
enableWaffoPancakeTopUp,
subscriptionLoading = false,
subscriptionPlans = [],
billingPreference,
@@ -105,6 +103,7 @@ const RechargeCard = ({
const [activeTab, setActiveTab] = useState('topup');
const shouldShowSubscription =
!subscriptionLoading && subscriptionPlans.length > 0;
const regularPayMethods = payMethods || [];
useEffect(() => {
if (initialTabSetRef.current) return;
@@ -227,19 +226,31 @@ const RechargeCard = ({
<div className='py-8 flex justify-center'>
<Spin size='large' />
</div>
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
) : enableOnlineTopUp ||
enableStripeTopUp ||
enableCreemTopUp ||
enableWaffoTopUp ||
enableWaffoPancakeTopUp ? (
<Form
getFormApi={(api) => (onlineFormApiRef.current = api)}
initValues={{ topUpCount: topUpCount }}
>
<div className='space-y-6'>
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
{(enableOnlineTopUp ||
enableStripeTopUp ||
enableWaffoTopUp ||
enableWaffoPancakeTopUp) && (
<Row gutter={12}>
<Col xs={24} sm={24} md={24} lg={10} xl={10}>
<Form.InputNumber
field='topUpCount'
label={t('充值数量')}
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWaffoTopUp}
disabled={
!enableOnlineTopUp &&
!enableStripeTopUp &&
!enableWaffoTopUp &&
!enableWaffoPancakeTopUp
}
placeholder={
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
}
@@ -291,16 +302,27 @@ const RechargeCard = ({
style={{ width: '100%' }}
/>
</Col>
{payMethods && payMethods.filter(m => m.type !== 'waffo').length > 0 && (
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
<Form.Slot label={t('选择支付方式')}>
{regularPayMethods.length > 0 && (
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
<Form.Slot label={t('选择支付方式')}>
<Space wrap>
{payMethods.filter(m => m.type !== 'waffo').map((payMethod) => {
const minTopupVal = Number(payMethod.min_topup) || 0;
{regularPayMethods.map((payMethod) => {
const minTopupVal =
Number(payMethod.min_topup) || 0;
const isStripe = payMethod.type === 'stripe';
const isWaffo =
typeof payMethod.type === 'string' &&
payMethod.type.startsWith('waffo:');
const isWaffoPancake =
payMethod.type === 'waffo_pancake';
const disabled =
(!enableOnlineTopUp && !isStripe) ||
(!enableOnlineTopUp &&
!isStripe &&
!isWaffo &&
!isWaffoPancake) ||
(!enableStripeTopUp && isStripe) ||
(!enableWaffoTopUp && isWaffo) ||
(!enableWaffoPancakeTopUp && isWaffoPancake) ||
minTopupVal > Number(topUpCount || 0);
const buttonEl = (
@@ -320,6 +342,21 @@ const RechargeCard = ({
<SiWechat size={18} color='#07C160' />
) : payMethod.type === 'stripe' ? (
<SiStripe size={18} color='#635BFF' />
) : payMethod.icon ? (
<img
src={payMethod.icon}
alt={payMethod.name}
style={{
width: 18,
height: 18,
objectFit: 'contain',
}}
/>
) : payMethod.type === 'waffo_pancake' ? (
<CreditCard
size={18}
color='var(--semi-color-primary)'
/>
) : (
<CreditCard
size={18}
@@ -355,8 +392,8 @@ const RechargeCard = ({
);
})}
</Space>
</Form.Slot>
</Col>
</Form.Slot>
</Col>
)}
</Row>
)}
@@ -388,7 +425,9 @@ const RechargeCard = ({
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'>
{presetAmounts.map((preset, index) => {
const discount =
preset.discount || topupInfo?.discount?.[preset.value] || 1.0;
preset.discount ||
topupInfo?.discount?.[preset.value] ||
1.0;
const originalPrice = preset.value * priceRatio;
const discountedPrice = originalPrice * discount;
const hasDiscount = discount < 1.0;
@@ -404,7 +443,7 @@ const RechargeCard = ({
const s = JSON.parse(statusStr);
usdRate = s?.usd_exchange_rate || 7;
}
} catch (e) { }
} catch (e) {}
let displayValue = preset.value; // 显示的数量
let displayActualPay = actualPay;
@@ -455,7 +494,10 @@ const RechargeCard = ({
{hasDiscount && (
<Tag style={{ marginLeft: 4 }} color='green'>
{t('折').includes('off')
? ((1 - parseFloat(discount)) * 100).toFixed(1)
? (
(1 - parseFloat(discount)) *
100
).toFixed(1)
: (discount * 10).toFixed(1)}
{t('折')}
</Tag>
@@ -482,46 +524,6 @@ const RechargeCard = ({
</Form.Slot>
)}
{/* Waffo 充值区域 */}
{enableWaffoTopUp &&
waffoPayMethods &&
waffoPayMethods.length > 0 && (
<Form.Slot label={t('Waffo 充值')}>
<Space wrap>
{waffoPayMethods.map((method, index) => (
<Button
key={index}
theme='outline'
type='tertiary'
onClick={() => waffoTopUp(index)}
loading={paymentLoading}
icon={
method.icon ? (
<img
src={method.icon}
alt={method.name}
style={{
width: 36,
height: 36,
objectFit: 'contain',
}}
/>
) : (
<CreditCard
size={18}
color='var(--semi-color-text-2)'
/>
)
}
className='!rounded-lg !px-4 !py-2'
>
{method.name}
</Button>
))}
</Space>
</Form.Slot>
)}
{/* Creem 充值区域 */}
{enableCreemTopUp && creemProducts.length > 0 && (
<Form.Slot label={t('Creem 充值')}>
+195 -35
View File
@@ -75,6 +75,8 @@ const TopUp = () => {
const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
const [waffoMinTopUp, setWaffoMinTopUp] = useState(1);
const [enableWaffoPancakeTopUp, setEnableWaffoPancakeTopUp] = useState(false);
const [waffoPancakeMinTopUp, setWaffoPancakeMinTopUp] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false);
@@ -112,6 +114,39 @@ const TopUp = () => {
discount: {},
});
const confirmPayMethods = [
...payMethods,
...waffoPayMethods.map((method, index) => ({
...method,
type: `waffo:${index}`,
min_topup: waffoMinTopUp,
color: method.color || 'rgba(var(--semi-primary-5), 1)',
})),
];
const getPayMethodConfig = (payment) =>
confirmPayMethods.find((method) => method.type === payment);
const getPaymentMinTopUp = (payment) => {
const configuredMinTopUp = Number(getPayMethodConfig(payment)?.min_topup);
return Number.isFinite(configuredMinTopUp) && configuredMinTopUp > 0
? configuredMinTopUp
: minTopUp;
};
const requestAmountByPayment = async (payment, value) => {
if (payment === 'stripe') {
return getStripeAmount(value);
}
if (payment === 'waffo_pancake') {
return getWaffoPancakeAmount(value);
}
if (typeof payment === 'string' && payment.startsWith('waffo:')) {
return getWaffoAmount(value);
}
return getAmount(value);
};
const topUp = async () => {
if (redemptionCode === '') {
showInfo(t('请输入兑换码!'));
@@ -162,6 +197,16 @@ const TopUp = () => {
showError(t('管理员未开启Stripe充值!'));
return;
}
} else if (payment === 'waffo_pancake') {
if (!enableWaffoPancakeTopUp) {
showError(t('管理员未开启 Waffo Pancake 充值!'));
return;
}
} else if (payment.startsWith('waffo:')) {
if (!enableWaffoTopUp) {
showError(t('管理员未开启 Waffo 充值!'));
return;
}
} else {
if (!enableOnlineTopUp) {
showError(t('管理员未开启在线充值!'));
@@ -172,14 +217,11 @@ const TopUp = () => {
setPayWay(payment);
setPaymentLoading(true);
try {
if (payment === 'stripe') {
await getStripeAmount();
} else {
await getAmount();
}
const selectedMinTopUp = getPaymentMinTopUp(payment);
await requestAmountByPayment(payment);
if (topUpCount < minTopUp) {
showError(t('充值数量不能小于') + minTopUp);
if (topUpCount < selectedMinTopUp) {
showError(t('充值数量不能小于') + selectedMinTopUp);
return;
}
setOpen(true);
@@ -191,6 +233,29 @@ const TopUp = () => {
};
const onlineTopUp = async () => {
if (payWay === 'waffo_pancake') {
setConfirmLoading(true);
try {
await waffoPancakeTopUp();
} finally {
setOpen(false);
setConfirmLoading(false);
}
return;
}
if (payWay.startsWith('waffo:')) {
const payMethodIndex = Number(payWay.split(':')[1]);
setConfirmLoading(true);
try {
await waffoTopUp(Number.isFinite(payMethodIndex) ? payMethodIndex : 0);
} finally {
setOpen(false);
setConfirmLoading(false);
}
return;
}
if (payWay === 'stripe') {
// Stripe 支付处理
if (amount === 0) {
@@ -317,32 +382,122 @@ const TopUp = () => {
const waffoTopUp = async (payMethodIndex) => {
try {
if (topUpCount < waffoMinTopUp) {
showError(t('充值数量不能小于') + waffoMinTopUp);
return;
}
setPaymentLoading(true);
const requestBody = {
amount: parseInt(topUpCount),
};
if (payMethodIndex != null) {
requestBody.pay_method_index = payMethodIndex;
}
const res = await API.post('/api/user/waffo/pay', requestBody);
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success' && data?.payment_url) {
window.open(data.payment_url, '_blank');
} else {
showError(data || t('支付请求失败'));
}
if (topUpCount < waffoMinTopUp) {
showError(t('充值数量不能小于') + waffoMinTopUp);
return;
}
setPaymentLoading(true);
const requestBody = {
amount: parseInt(topUpCount),
};
if (payMethodIndex != null) {
requestBody.pay_method_index = payMethodIndex;
}
const res = await API.post('/api/user/waffo/pay', requestBody);
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success' && data?.payment_url) {
window.open(data.payment_url, '_blank');
} else {
showError(res);
showError(data || t('支付请求失败'));
}
} else {
showError(res);
}
} catch (e) {
showError(t('支付请求失败'));
showError(t('支付请求失败'));
} finally {
setPaymentLoading(false);
setPaymentLoading(false);
}
};
const getWaffoAmount = async (value) => {
if (value === undefined) {
value = topUpCount;
}
setAmountLoading(true);
try {
const res = await API.post('/api/user/waffo/amount', {
amount: parseInt(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) {
// amount fetch failed silently
} finally {
setAmountLoading(false);
}
};
const waffoPancakeTopUp = async () => {
const minTopUpValue = Number(waffoPancakeMinTopUp || 1);
if (topUpCount < minTopUpValue) {
showError(t('充值数量不能小于') + minTopUpValue);
return;
}
setPaymentLoading(true);
try {
const res = await API.post('/api/user/waffo-pancake/pay', {
amount: parseInt(topUpCount),
});
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success') {
const checkoutUrl = data?.checkout_url || '';
if (checkoutUrl) {
window.open(checkoutUrl, '_blank');
} else {
showError(t('支付请求失败'));
}
} else {
const errorMsg =
typeof data === 'string' ? data : message || t('支付请求失败');
showError(errorMsg);
}
} else {
showError(res);
}
} catch (e) {
showError(t('支付请求失败'));
} finally {
setPaymentLoading(false);
}
};
const getWaffoPancakeAmount = async (value) => {
if (value === undefined) {
value = topUpCount;
}
setAmountLoading(true);
try {
const res = await API.post('/api/user/waffo-pancake/amount', {
amount: parseInt(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) {
// amount fetch failed silently
} finally {
setAmountLoading(false);
}
};
@@ -481,20 +636,26 @@ const TopUp = () => {
const enableStripeTopUp = data.enable_stripe_topup || false;
const enableOnlineTopUp = data.enable_online_topup || false;
const enableCreemTopUp = data.enable_creem_topup || false;
const enableWaffoTopUp = data.enable_waffo_topup || false;
const enableWaffoPancakeTopUp =
data.enable_waffo_pancake_topup || false;
const minTopUpValue = enableOnlineTopUp
? data.min_topup
: enableStripeTopUp
? data.stripe_min_topup
: data.enable_waffo_topup
: enableWaffoTopUp
? data.waffo_min_topup
: enableWaffoPancakeTopUp
? data.waffo_pancake_min_topup
: 1;
setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp);
const enableWaffoTopUp = data.enable_waffo_topup || false;
setEnableWaffoTopUp(enableWaffoTopUp);
setWaffoPayMethods(data.waffo_pay_methods || []);
setWaffoMinTopUp(data.waffo_min_topup || 1);
setEnableWaffoPancakeTopUp(enableWaffoPancakeTopUp);
setWaffoPancakeMinTopUp(data.waffo_pancake_min_topup || 1);
setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue);
@@ -739,7 +900,7 @@ const TopUp = () => {
amountLoading={amountLoading}
renderAmount={renderAmount}
payWay={payWay}
payMethods={payMethods}
payMethods={confirmPayMethods}
amountNumber={amount}
discountRate={topupInfo?.discount?.[topUpCount] || 1.0}
/>
@@ -789,8 +950,7 @@ const TopUp = () => {
creemProducts={creemProducts}
creemPreTopUp={creemPreTopUp}
enableWaffoTopUp={enableWaffoTopUp}
waffoTopUp={waffoTopUp}
waffoPayMethods={waffoPayMethods}
enableWaffoPancakeTopUp={enableWaffoPancakeTopUp}
presetAmounts={presetAmounts}
selectedPreset={selectedPreset}
selectPresetAmount={selectPresetAmount}
@@ -804,7 +964,7 @@ const TopUp = () => {
setSelectedPreset={setSelectedPreset}
renderAmount={renderAmount}
amountLoading={amountLoading}
payMethods={payMethods}
payMethods={confirmPayMethods}
preTopUp={preTopUp}
paymentLoading={paymentLoading}
payWay={payWay}
@@ -140,6 +140,17 @@ const PaymentConfirmModal = ({
size={16}
color='#635BFF'
/>
) : payMethod.icon ? (
<img
src={payMethod.icon}
alt={payMethod.name}
className='mr-2'
style={{
width: 16,
height: 16,
objectFit: 'contain',
}}
/>
) : (
<CreditCard
className='mr-2'
+8 -4
View File
@@ -14,7 +14,9 @@
",点击更新": ", click Update",
"(共 {{total}} 个,省略 {{omit}} 个)": "",
"(共 {{total}} 个)": "",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "Currently only the Epay interface is supported. Configure the callback address in General Settings.",
"请确认商户和所选环境密钥一致。": "Make sure the merchant and keys for the selected environment match.",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "Make sure Merchant, Store, Product, and the keys for the selected environment match.",
"(筛选后显示 {{count}} 条)_one": "(Showing {{count}} item after filtering)",
"(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Input {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
@@ -743,6 +745,8 @@
"最低": "lowest",
"最低充值数量": "",
"最低充值美元数量": "Minimum recharge dollar amount",
"最低充值美元数量必须大于 0": "Minimum recharge dollar amount must be greater than 0",
"留空则自动使用当前站点的默认回调地址": "Leave blank to use the default callback address of the current site",
"最后使用时间": "Last used time",
"最后更新": "Last Updated",
"最后请求": "Last request",
@@ -1044,7 +1048,7 @@
"响应缺少凭据": "Response missing credentials",
"响应缺少授权链接": "Response missing authorization link",
"商品价格 ID": "Product Price ID",
"商户 ID": "",
"商户 ID": "Merchant ID",
"回答内容": "Answer Content",
"回调 URL 填": "Callback URL Fill",
"回调 URL 格式": "Callback URL format",
@@ -1712,7 +1716,7 @@
"支付渠道": "Payment Channels",
"支付设置": "Payment",
"支付请求失败": "Payment request failed",
"支付返回地址": "",
"支付返回地址": "Return URL",
"支付金额": "Payment Amount",
"支持 Ctrl+V 粘贴图片": "Supports Ctrl+V to paste images",
"支持 JSONPath,如 email, data.user.email": "Supports JSONPath, e.g. email, data.user.email",
@@ -2212,7 +2216,7 @@
"永久删除您的两步验证设置": "Permanently delete your two-factor authentication settings",
"永久删除所有备用码(包括未使用的)": "Permanently delete all backup codes (including unused ones)",
"汇率": "Exchange rate",
"沙盒模式": "",
"沙盒模式": "Sandbox Mode",
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Sandbox RSA private key Base64 (PKCS#8 DER)",
"沙盒环境 Waffo API 密钥": "",
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Sandbox Waffo public key Base64 (X.509 DER)",
+8 -4
View File
@@ -16,7 +16,9 @@
",点击更新": ", cliquez sur Mettre à jour",
"(共 {{total}} 个,省略 {{omit}} 个)": "",
"(共 {{total}} 个)": "",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Actuellement, seule l'interface Epay est prise en charge, l'adresse du serveur ci-dessus est utilisée par défaut comme adresse de rappel !)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "Seule l'interface Epay est actuellement prise en charge. Configurez l'adresse de rappel dans les paramètres généraux.",
"请确认商户和所选环境密钥一致。": "Vérifiez que le marchand et les clés de l'environnement sélectionné correspondent.",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "Vérifiez que Merchant, Store, Product et les clés de l'environnement sélectionné correspondent.",
"(筛选后显示 {{count}} 条)_one": "(Showing {{count}} item after filtering)",
"(筛选后显示 {{count}} 条)_many": "(Affichage de {{count}} éléments après filtrage)",
"(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)",
@@ -742,6 +744,8 @@
"最低": "Le plus bas",
"最低充值数量": "",
"最低充值美元数量": "Montant minimum de recharge en dollars",
"最低充值美元数量必须大于 0": "Le montant minimum de recharge en dollars doit être supérieur à 0",
"留空则自动使用当前站点的默认回调地址": "Laissez vide pour utiliser l'adresse de rappel par défaut du site actuel",
"最后使用时间": "Dernière utilisation",
"最后更新": "Last Updated",
"最后请求": "Dernière requête",
@@ -1042,7 +1046,7 @@
"响应缺少凭据": "Identifiants manquants dans la réponse",
"响应缺少授权链接": "Lien d'autorisation manquant dans la réponse",
"商品价格 ID": "ID du prix du produit",
"商户 ID": "",
"商户 ID": "ID marchand",
"回答内容": "Contenu de la réponse",
"回调 URL 填": "Remplir l'URL de rappel",
"回调 URL 格式": "Format de l'URL de rappel",
@@ -1712,7 +1716,7 @@
"支付渠道": "Canaux de paiement",
"支付设置": "Paiement",
"支付请求失败": "Échec de la demande de paiement",
"支付返回地址": "",
"支付返回地址": "URL de retour",
"支付金额": "Montant payé",
"支持 Ctrl+V 粘贴图片": "Supporte Ctrl+V pour coller l'image",
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Prend en charge le code de vérification TOTP à 6 chiffres ou le code de sauvegarde à 8 chiffres, peut être configuré ou consulté dans `Paramètres personnels - Paramètres de sécurité - Paramètres d'authentification à deux facteurs`.",
@@ -2203,7 +2207,7 @@
"永久删除您的两步验证设置": "Supprimer définitivement vos paramètres d'authentification à deux facteurs",
"永久删除所有备用码(包括未使用的)": "Supprimer définitivement tous les codes de sauvegarde (y compris ceux non utilisés)",
"汇率": "Taux de change",
"沙盒模式": "",
"沙盒模式": "Mode bac à sable",
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de sandbox",
"沙盒环境 Waffo API 密钥": "",
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de sandbox",
+8 -4
View File
@@ -14,7 +14,9 @@
",点击更新": "、クリックして更新してください",
"(共 {{total}} 个,省略 {{omit}} 个)": "",
"(共 {{total}} 个)": "",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "現在Epay APIのみ対応しています。デフォルトで、上記のサーバーURLがコールバックアドレスとして使用されます。)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "現在Epay API のみ対応しています。コールバックアドレスは一般設定で設定してください。",
"请确认商户和所选环境密钥一致。": "加盟店情報と選択中の環境の鍵が一致していることを確認してください。",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "Merchant、Store、Product と選択中の環境の鍵が一致していることを確認してください。",
"(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(入力 {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(入力 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + オーディオ入力 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}",
@@ -729,6 +731,8 @@
"最低": "最低",
"最低充值数量": "",
"最低充值美元数量": "最低チャージUSD額",
"最低充值美元数量必须大于 0": "最低チャージUSD額は 0 より大きい必要があります",
"留空则自动使用当前站点的默认回调地址": "空欄の場合は現在のサイトのデフォルトのコールバックアドレスを使用します",
"最后使用时间": "最終利用日時",
"最后更新": "Last Updated",
"最后请求": "最終リクエスト日時",
@@ -1029,7 +1033,7 @@
"响应缺少凭据": "レスポンスに資格情報がありません",
"响应缺少授权链接": "レスポンスに認可リンクがありません",
"商品价格 ID": "料金ID",
"商户 ID": "",
"商户 ID": "加盟店 ID",
"回答内容": "回答",
"回调 URL 填": "コールバックURLを入力してください",
"回调 URL 格式": "コールバックURL形式",
@@ -1683,7 +1687,7 @@
"支付渠道": "決済チャネル",
"支付设置": "決済",
"支付请求失败": "決済リクエストに失敗しました",
"支付返回地址": "",
"支付返回地址": "返却先 URL",
"支付金额": "決済金額",
"支持 Ctrl+V 粘贴图片": "Ctrl+V で画像を貼り付け可能",
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "6桁のTOTP認証コードまたは8桁のバックアップコードに対応しています。`アカウント設定 - セキュリティ設定 - 2要素認証設定`で設定または確認できます。",
@@ -2174,7 +2178,7 @@
"永久删除您的两步验证设置": "2要素認証設定を永久に削除",
"永久删除所有备用码(包括未使用的)": "すべてのバックアップコード(未使用分を含む)を永久に削除",
"汇率": "為替レート",
"沙盒模式": "",
"沙盒模式": "サンドボックスモード",
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "サンドボックス RSA 秘密鍵 Base64 (PKCS#8 DER)",
"沙盒环境 Waffo API 密钥": "",
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "サンドボックス Waffo 公開鍵 Base64 (X.509 DER)",
+8 -4
View File
@@ -18,7 +18,9 @@
",点击更新": ", нажмите для обновления",
"(共 {{total}} 个,省略 {{omit}} 个)": "",
"(共 {{total}} 个)": "",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(В настоящее время поддерживается только интерфейс YiPay, по умолчанию используется адрес сервера выше в качестве адреса обратного вызова!)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "Сейчас поддерживается только интерфейс Epay. Настройте адрес обратного вызова в общих настройках.",
"请确认商户和所选环境密钥一致。": "Убедитесь, что мерчант и ключи выбранной среды совпадают.",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "Убедитесь, что Merchant, Store, Product и ключи выбранной среды совпадают.",
"(筛选后显示 {{count}} 条)_one": "(Showing {{count}} item after filtering)",
"(筛选后显示 {{count}} 条)_few": "(Показано {{count}} элемента после фильтрации)",
"(筛选后显示 {{count}} 条)_many": "(Показано {{count}} элементов после фильтрации)",
@@ -750,6 +752,8 @@
"最低": "Минимум",
"最低充值数量": "",
"最低充值美元数量": "Минимальная сумма пополнения в долларах",
"最低充值美元数量必须大于 0": "Минимальная сумма пополнения в долларах должна быть больше 0",
"留空则自动使用当前站点的默认回调地址": "Оставьте пустым, чтобы использовать адрес обратного вызова сайта по умолчанию",
"最后使用时间": "Время последнего использования",
"最后更新": "Last Updated",
"最后请求": "Последний запрос",
@@ -1050,7 +1054,7 @@
"响应缺少凭据": "В ответе отсутствуют учётные данные",
"响应缺少授权链接": "В ответе отсутствует ссылка авторизации",
"商品价格 ID": "ID цены товара",
"商户 ID": "",
"商户 ID": "ID мерчанта",
"回答内容": "Содержание ответа",
"回调 URL 填": "URL обратного вызова",
"回调 URL 格式": "Формат URL обратного вызова",
@@ -1730,7 +1734,7 @@
"支付渠道": "Платежные каналы",
"支付设置": "Оплата",
"支付请求失败": "Запрос на оплату не удался",
"支付返回地址": "",
"支付返回地址": "Адрес возврата",
"支付金额": "Сумма оплаты",
"支持 Ctrl+V 粘贴图片": "Поддержка Ctrl+V для вставки изображения",
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Поддерживает 6-значные TOTP коды подтверждения или 8-значные резервные коды, можно настроить или просмотреть в `Личные настройки-Настройки безопасности-Настройки двухфакторной аутентификации`.",
@@ -2221,7 +2225,7 @@
"永久删除您的两步验证设置": "Окончательно удалить настройки двухфакторной аутентификации",
"永久删除所有备用码(包括未使用的)": "Окончательно удалить все резервные коды (включая неиспользованные)",
"汇率": "Обменный курс",
"沙盒模式": "",
"沙盒模式": "Режим песочницы",
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) песочницы",
"沙盒环境 Waffo API 密钥": "",
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) песочницы",
+8 -4
View File
@@ -14,7 +14,9 @@
",点击更新": ", nhấn để cập nhật",
"(共 {{total}} 个,省略 {{omit}} 个)": "",
"(共 {{total}} 个)": "",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Hiện tại chỉ hỗ trợ giao diện Epay, địa chỉ máy chủ phía trên được sử dụng làm địa chỉ gọi lại theo mặc định!)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "Hiện tại chỉ hỗ trợ giao diện Epay. Hãy cấu hình địa chỉ gọi lại trong cài đặt chung.",
"请确认商户和所选环境密钥一致。": "Hãy đảm bảo merchant và khóa của môi trường đã chọn khớp nhau.",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "Hãy đảm bảo Merchant, Store, Product và khóa của môi trường đã chọn khớp nhau.",
"(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Đầu vào {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Đầu vào {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + Đầu vào âm thanh {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}",
@@ -730,6 +732,8 @@
"最低": "thấp nhất",
"最低充值数量": "",
"最低充值美元数量": "Số tiền nạp đô la tối thiểu",
"最低充值美元数量必须大于 0": "Số tiền nạp đô la tối thiểu phải lớn hơn 0",
"留空则自动使用当前站点的默认回调地址": "Để trống để dùng địa chỉ gọi lại mặc định của trang hiện tại",
"最后使用时间": "Thời gian sử dụng cuối cùng",
"最后更新": "Last Updated",
"最后请求": "Yêu cầu cuối cùng",
@@ -1030,7 +1034,7 @@
"响应缺少凭据": "Phản hồi thiếu thông tin xác thực",
"响应缺少授权链接": "Phản hồi thiếu liên kết xác thực",
"商品价格 ID": "ID giá sản phẩm",
"商户 ID": "",
"商户 ID": "ID người bán",
"回答内容": "Nội dung trả lời",
"回调 URL 填": "Điền URL gọi lại",
"回调 URL 格式": "Định dạng URL callback",
@@ -1684,7 +1688,7 @@
"支付渠道": "Kênh thanh toán",
"支付设置": "Thanh toán",
"支付请求失败": "Yêu cầu thanh toán thất bại",
"支付返回地址": "",
"支付返回地址": "URL trả về",
"支付金额": "Số tiền thanh toán",
"支持 Ctrl+V 粘贴图片": "Hỗ trợ Ctrl+V để dán hình ảnh",
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Hỗ trợ mã xác minh TOTP 6 chữ số hoặc mã dự phòng 8 chữ số, có thể được cấu hình hoặc xem trong `Cài đặt cá nhân - Cài đặt bảo mật - Cài đặt xác thực hai yếu tố`.",
@@ -2211,7 +2215,7 @@
"永久删除所有备用码(包括未使用的)": "Xóa vĩnh viễn tất cả các mã dự phòng (bao gồm cả mã chưa sử dụng)",
"永久有效": "Có hiệu lực vĩnh viễn",
"汇率": "Tỷ giá hối đoái",
"沙盒模式": "",
"沙盒模式": "Chế độ Sandbox",
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sandbox",
"沙盒环境 Waffo API 密钥": "",
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sandbox",
+45 -1
View File
@@ -12,7 +12,9 @@
",点击更新": ",点击更新",
"(共 {{total}} 个,省略 {{omit}} 个)": "(共 {{total}} 个,省略 {{omit}} 个)",
"(共 {{total}} 个)": "(共 {{total}} 个)",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "当前仅支持易支付接口,回调地址请在通用设置中配置。",
"请确认商户和所选环境密钥一致。": "请确认商户和所选环境密钥一致。",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "请确认 Merchant、Store、Product 和所选环境密钥一致。",
"(筛选后显示 {{count}} 条)_other": "(筛选后显示 {{count}} 条)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}",
@@ -718,6 +720,8 @@
"最低": "最低",
"最低充值数量": "最低充值数量",
"最低充值美元数量": "最低充值美元数量",
"最低充值美元数量必须大于 0": "最低充值美元数量必须大于 0",
"留空则自动使用当前站点的默认回调地址": "留空则自动使用当前站点的默认回调地址",
"最后使用时间": "最后使用时间",
"最后更新": "最后更新",
"最后请求": "最后请求",
@@ -1671,6 +1675,7 @@
"支付方式类型": "支付方式类型",
"支付渠道": "支付渠道",
"支付设置": "支付设置",
"易支付设置": "易支付设置",
"支付请求失败": "支付请求失败",
"支付返回地址": "支付返回地址",
"支付金额": "支付金额",
@@ -1895,6 +1900,7 @@
"更新成功": "更新成功",
"更新所有已启用通道余额": "更新所有已启用通道余额",
"更新支付设置": "更新支付设置",
"更新易支付设置": "更新易支付设置",
"更新时间": "更新时间",
"更新服务器地址": "更新服务器地址",
"更新模型信息": "更新模型信息",
@@ -3194,6 +3200,44 @@
"豆包": "豆包",
"账单": "账单",
"账户充值": "账户充值",
"Waffo Pancake 设置": "Waffo Pancake 设置",
"Waffo 设置": "Waffo 设置",
"Waffo Pancake": "Waffo Pancake",
"启用 Waffo Pancake": "启用 Waffo Pancake",
"当前入口状态": "当前入口状态",
"生产环境": "生产环境",
"测试环境": "测试环境",
"支付方式名称": "支付方式名称",
"支付方式颜色": "支付方式颜色",
"支付方式图标": "支付方式图标",
"可选,填写图片 URL": "可选,填写图片 URL",
"商户 ID": "商户 ID",
"Store ID": "Store ID",
"Product ID": "Product ID",
"API 私钥": "API 私钥",
"Webhook 公钥": "Webhook 公钥",
"充值价格必须大于 0": "充值价格必须大于 0",
"最低充值数量必须大于 0": "最低充值数量必须大于 0",
"充值完成后跳回的页面": "充值完成后跳回的页面",
"启用后会按测试环境保存这组配置": "启用后会按测试环境保存这组配置",
"更新 Waffo Pancake 设置": "更新 Waffo Pancake 设置",
"一次性余额充值": "一次性余额充值",
"新支付方式": "新支付方式",
"付款完成后将自动回到账户页": "付款完成后将自动回到账户页",
"一次性支付,付款后自动返回": "一次性支付,付款后自动返回",
"选择金额后直接跳转到 Waffo Pancake 结账页,支付完成后会回到账户页。": "选择金额后直接跳转到 Waffo Pancake 结账页,支付完成后会回到账户页。",
"当前金额未达到 Waffo Pancake 的最低充值要求": "当前金额未达到 Waffo Pancake 的最低充值要求",
"请先选择不低于最低额度的充值金额": "请先选择不低于最低额度的充值金额",
"该入口仅用于一次性余额充值": "该入口仅用于一次性余额充值",
"立即充值": "立即充值",
"生产 Webhook 公钥": "生产 Webhook 公钥",
"测试 Webhook 公钥": "测试 Webhook 公钥",
"生产环境 Webhook 验签公钥 Base64": "生产环境 Webhook 验签公钥 Base64",
"测试环境 Webhook 验签公钥 Base64": "测试环境 Webhook 验签公钥 Base64",
"请输入支付方式名称": "请输入支付方式名称",
"请输入商户 ID": "请输入商户 ID",
"请输入 Store ID": "请输入 Store ID",
"请输入 Product ID": "请输入 Product ID",
"账户已删除!": "账户已删除!",
"账户已锁定": "账户已锁定",
"账户数据": "账户数据",
+8 -4
View File
@@ -12,7 +12,9 @@
",点击更新": ",點擊更新",
"(共 {{total}} 个,省略 {{omit}} 个)": "",
"(共 {{total}} 个)": "",
"当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(當前僅支援易支付接口,預設使用上方伺服器位址作為回調位址!)",
"当前仅支持易支付接口,回调地址请在通用设置中配置。": "前僅支援易支付接口,回調位址請在通用設定中配置。",
"请确认商户和所选环境密钥一致。": "請確認商戶與所選環境密鑰一致。",
"请确认 Merchant、Store、Product 和所选环境密钥一致。": "請確認 Merchant、Store、Product 與所選環境密鑰一致。",
"(筛选后显示 {{count}} 条)_other": "(篩選後顯示 {{count}} 條)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(輸入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(輸入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音訊輸入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}",
@@ -726,6 +728,8 @@
"最低": "最低",
"最低充值数量": "",
"最低充值美元数量": "最低儲值美元數量",
"最低充值美元数量必须大于 0": "最低儲值美元數量必須大於 0",
"留空则自动使用当前站点的默认回调地址": "留空則自動使用目前站點的預設回調位址",
"最后使用时间": "最後使用時間",
"最后更新": "最後更新",
"最后请求": "最後請求",
@@ -1027,7 +1031,7 @@
"响应缺少凭据": "",
"响应缺少授权链接": "",
"商品价格 ID": "商品價格 ID",
"商户 ID": "",
"商户 ID": "商戶 ID",
"回答内容": "回答內容",
"回调 URL 填": "回調 URL 填",
"回调 URL 格式": "回調 URL 格式",
@@ -1683,7 +1687,7 @@
"支付渠道": "支付管道",
"支付设置": "支付設定",
"支付请求失败": "支付請求失敗",
"支付返回地址": "",
"支付返回地址": "支付返回位址",
"支付金额": "支付金額",
"支持 Ctrl+V 粘贴图片": "支援 Ctrl+V 貼上圖片",
"支持 JSONPath,如 email, data.user.email": "支援 JSONPath,如 email, data.user.email",
@@ -2181,7 +2185,7 @@
"永久删除您的两步验证设置": "永久刪除您的兩步驗證設定",
"永久删除所有备用码(包括未使用的)": "永久刪除所有備用碼(包括未使用的)",
"汇率": "匯率",
"沙盒模式": "",
"沙盒模式": "沙盒模式",
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "沙盒環境 RSA 私鑰 Base64 (PKCS#8 DER)",
"沙盒环境 Waffo API 密钥": "",
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "沙盒環境 Waffo 公鑰 Base64 (X.509 DER)",
@@ -18,29 +18,43 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import { Button, Form, Spin } from '@douyinfe/semi-ui';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
API,
removeTrailingSlash,
showError,
showSuccess,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsGeneralPayment(props) {
const { t } = useTranslation();
const sectionTitle = props.hideSectionTitle ? undefined : t('通用设置');
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ServerAddress: '',
CustomCallbackAddress: '',
TopupGroupRatio: '',
PayMethods: '',
AmountOptions: '',
AmountDiscount: '',
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);
useEffect(() => {
if (props.options && formApiRef.current) {
const currentInputs = {
ServerAddress: props.options.ServerAddress || '',
CustomCallbackAddress: props.options.CustomCallbackAddress || '',
TopupGroupRatio: props.options.TopupGroupRatio || '',
PayMethods: props.options.PayMethods || '',
AmountOptions: props.options.AmountOptions || '',
AmountDiscount: props.options.AmountDiscount || '',
};
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
formApiRef.current.setValues(currentInputs);
}
}, [props.options]);
@@ -49,19 +63,93 @@ export default function SettingsGeneralPayment(props) {
setInputs(values);
};
const submitServerAddress = async () => {
const submitGeneralSettings = async () => {
if (
originInputs.TopupGroupRatio !== inputs.TopupGroupRatio &&
!verifyJSON(inputs.TopupGroupRatio)
) {
showError(t('充值分组倍率不是合法的 JSON 字符串'));
return;
}
if (
originInputs.PayMethods !== inputs.PayMethods &&
!verifyJSON(inputs.PayMethods)
) {
showError(t('充值方式设置不是合法的 JSON 字符串'));
return;
}
if (
originInputs.AmountOptions !== inputs.AmountOptions &&
inputs.AmountOptions.trim() !== '' &&
!verifyJSON(inputs.AmountOptions)
) {
showError(t('自定义充值数量选项不是合法的 JSON 数组'));
return;
}
if (
originInputs.AmountDiscount !== inputs.AmountDiscount &&
inputs.AmountDiscount.trim() !== '' &&
!verifyJSON(inputs.AmountDiscount)
) {
showError(t('充值金额折扣配置不是合法的 JSON 对象'));
return;
}
setLoading(true);
try {
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
const res = await API.put('/api/option/', {
key: 'ServerAddress',
value: ServerAddress,
});
if (res.data.success) {
const options = [
{
key: 'ServerAddress',
value: removeTrailingSlash(inputs.ServerAddress),
},
];
if (inputs.CustomCallbackAddress !== '') {
options.push({
key: 'CustomCallbackAddress',
value: removeTrailingSlash(inputs.CustomCallbackAddress),
});
}
if (originInputs.TopupGroupRatio !== inputs.TopupGroupRatio) {
options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
}
if (originInputs.PayMethods !== inputs.PayMethods) {
options.push({ key: 'PayMethods', value: inputs.PayMethods });
}
if (originInputs.AmountOptions !== inputs.AmountOptions) {
options.push({
key: 'payment_setting.amount_options',
value: inputs.AmountOptions,
});
}
if (originInputs.AmountDiscount !== inputs.AmountDiscount) {
options.push({
key: 'payment_setting.amount_discount',
value: inputs.AmountDiscount,
});
}
const results = await Promise.all(
options.map((option) =>
API.put('/api/option/', {
key: option.key,
value: option.value,
}),
),
);
const errorResults = results.filter((res) => !res.data.success);
if (errorResults.length === 0) {
showSuccess(t('更新成功'));
setOriginInputs({ ...inputs });
props.refresh && props.refresh();
} else {
showError(res.data.message);
errorResults.forEach((res) => {
showError(res.data.message);
});
}
} catch (error) {
showError(t('更新失败'));
@@ -76,7 +164,7 @@ export default function SettingsGeneralPayment(props) {
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={t('通用设置')}>
<Form.Section text={sectionTitle}>
<Form.Input
field='ServerAddress'
label={t('服务器地址')}
@@ -86,7 +174,73 @@ export default function SettingsGeneralPayment(props) {
'该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
)}
/>
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='CustomCallbackAddress'
label={t('回调地址')}
placeholder={t('例如:https://yourdomain.com')}
extraText={t(
'留空时默认使用服务器地址作为回调地址,填写后将覆盖默认值',
)}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='TopupGroupRatio'
label={t('充值分组倍率')}
placeholder={t('为一个 JSON 文本,键为组名称,值为倍率')}
autosize
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='PayMethods'
label={t('充值方式设置')}
placeholder={t('为一个 JSON 文本')}
autosize
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='AmountOptions'
label={t('自定义充值数量选项')}
placeholder={t(
'为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]',
)}
autosize
extraText={t(
'设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]',
)}
/>
</Col>
</Row>
<Row style={{ marginTop: 16 }}>
<Col span={24}>
<Form.TextArea
field='AmountDiscount'
label={t('充值金额折扣配置')}
placeholder={t(
'为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
autosize
extraText={t(
'设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
/>
</Col>
</Row>
<Button onClick={submitGeneralSettings} style={{ marginTop: 16 }}>
{t('保存通用设置')}
</Button>
</Form.Section>
</Form>
</Spin>
@@ -18,19 +18,19 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import { Button, Form, Row, Col, Typography, Spin } from '@douyinfe/semi-ui';
const { Text } = Typography;
import { Banner, Button, Form, Row, Col, Spin } from '@douyinfe/semi-ui';
import {
API,
removeTrailingSlash,
showError,
showSuccess,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
import { Info } from 'lucide-react';
export default function SettingsPaymentGateway(props) {
const { t } = useTranslation();
const sectionTitle = props.hideSectionTitle ? undefined : t('易支付设置');
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
PayAddress: '',
@@ -38,13 +38,7 @@ export default function SettingsPaymentGateway(props) {
EpayKey: '',
Price: 7.3,
MinTopUp: 1,
TopupGroupRatio: '',
CustomCallbackAddress: '',
PayMethods: '',
AmountOptions: '',
AmountDiscount: '',
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);
useEffect(() => {
@@ -61,35 +55,9 @@ export default function SettingsPaymentGateway(props) {
props.options.MinTopUp !== undefined
? parseFloat(props.options.MinTopUp)
: 1,
TopupGroupRatio: props.options.TopupGroupRatio || '',
CustomCallbackAddress: props.options.CustomCallbackAddress || '',
PayMethods: props.options.PayMethods || '',
AmountOptions: props.options.AmountOptions || '',
AmountDiscount: props.options.AmountDiscount || '',
};
// 美化 JSON 展示
try {
if (currentInputs.AmountOptions) {
currentInputs.AmountOptions = JSON.stringify(
JSON.parse(currentInputs.AmountOptions),
null,
2,
);
}
} catch {}
try {
if (currentInputs.AmountDiscount) {
currentInputs.AmountDiscount = JSON.stringify(
JSON.parse(currentInputs.AmountDiscount),
null,
2,
);
}
} catch {}
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
formApiRef.current.setValues(currentInputs);
}
}, [props.options]);
@@ -104,40 +72,6 @@ export default function SettingsPaymentGateway(props) {
return;
}
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
if (!verifyJSON(inputs.TopupGroupRatio)) {
showError(t('充值分组倍率不是合法的 JSON 字符串'));
return;
}
}
if (originInputs['PayMethods'] !== inputs.PayMethods) {
if (!verifyJSON(inputs.PayMethods)) {
showError(t('充值方式设置不是合法的 JSON 字符串'));
return;
}
}
if (
originInputs['AmountOptions'] !== inputs.AmountOptions &&
inputs.AmountOptions.trim() !== ''
) {
if (!verifyJSON(inputs.AmountOptions)) {
showError(t('自定义充值数量选项不是合法的 JSON 数组'));
return;
}
}
if (
originInputs['AmountDiscount'] !== inputs.AmountDiscount &&
inputs.AmountDiscount.trim() !== ''
) {
if (!verifyJSON(inputs.AmountDiscount)) {
showError(t('充值金额折扣配置不是合法的 JSON 对象'));
return;
}
}
setLoading(true);
try {
const options = [
@@ -156,32 +90,7 @@ export default function SettingsPaymentGateway(props) {
if (inputs.MinTopUp !== '') {
options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
}
if (inputs.CustomCallbackAddress !== '') {
options.push({
key: 'CustomCallbackAddress',
value: inputs.CustomCallbackAddress,
});
}
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
}
if (originInputs['PayMethods'] !== inputs.PayMethods) {
options.push({ key: 'PayMethods', value: inputs.PayMethods });
}
if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
options.push({
key: 'payment_setting.amount_options',
value: inputs.AmountOptions,
});
}
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) {
options.push({
key: 'payment_setting.amount_discount',
value: inputs.AmountDiscount,
});
}
// 发送请求
const requestQueue = options.map((opt) =>
API.put('/api/option/', {
key: opt.key,
@@ -191,7 +100,6 @@ export default function SettingsPaymentGateway(props) {
const results = await Promise.all(requestQueue);
// 检查所有请求是否成功
const errorResults = results.filter((res) => !res.data.success);
if (errorResults.length > 0) {
errorResults.forEach((res) => {
@@ -199,8 +107,6 @@ export default function SettingsPaymentGateway(props) {
});
} else {
showSuccess(t('更新成功'));
// 更新本地存储的原始值
setOriginInputs({ ...inputs });
props.refresh && props.refresh();
}
} catch (error) {
@@ -216,12 +122,15 @@ export default function SettingsPaymentGateway(props) {
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={t('支付设置')}>
<Text>
{t(
'(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)',
<Form.Section text={sectionTitle}>
<Banner
type='info'
icon={<Info size={16} />}
description={t(
'当前仅支持易支付接口,回调地址请在通用设置中配置。',
)}
</Text>
style={{ marginBottom: 16 }}
/>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
@@ -233,14 +142,14 @@ export default function SettingsPaymentGateway(props) {
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='EpayId'
label={t('易支付商户ID')}
label={t('商户 ID')}
placeholder={t('例如:0001')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='EpayKey'
label={t('易支付商户密钥')}
label={t('API 密钥')}
placeholder={t('敏感信息不会发送到前端显示')}
type='password'
/>
@@ -250,14 +159,7 @@ export default function SettingsPaymentGateway(props) {
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='CustomCallbackAddress'
label={t('回调地址')}
placeholder={t('例如:https://yourdomain.com')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.InputNumber
field='Price'
precision={2}
@@ -265,7 +167,7 @@ export default function SettingsPaymentGateway(props) {
placeholder={t('例如:7,就是7元/美金')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.InputNumber
field='MinTopUp'
label={t('最低充值美元数量')}
@@ -273,58 +175,9 @@ export default function SettingsPaymentGateway(props) {
/>
</Col>
</Row>
<Form.TextArea
field='TopupGroupRatio'
label={t('充值分组倍率')}
placeholder={t('为一个 JSON 文本,键为组名称,值为倍率')}
autosize
/>
<Form.TextArea
field='PayMethods'
label={t('充值方式设置')}
placeholder={t('为一个 JSON 文本')}
autosize
/>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col span={24}>
<Form.TextArea
field='AmountOptions'
label={t('自定义充值数量选项')}
placeholder={t(
'为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]',
)}
autosize
extraText={t(
'设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]',
)}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col span={24}>
<Form.TextArea
field='AmountDiscount'
label={t('充值金额折扣配置')}
placeholder={t(
'为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
autosize
extraText={t(
'设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
/>
</Col>
</Row>
<Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
<Button onClick={submitPayAddress} style={{ marginTop: 16 }}>
{t('更新易支付设置')}
</Button>
</Form.Section>
</Form>
</Spin>
@@ -34,10 +34,11 @@ import {
const { Text } = Typography;
import { API, showError, showSuccess } from '../../../helpers';
import { useTranslation } from 'react-i18next';
import { Plus, Trash2 } from 'lucide-react';
import { BookOpen, Plus, Trash2 } from 'lucide-react';
export default function SettingsPaymentGatewayCreem(props) {
const { t } = useTranslation();
const sectionTitle = props.hideSectionTitle ? undefined : t('Creem 设置');
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
CreemApiKey: '',
@@ -259,15 +260,22 @@ export default function SettingsPaymentGatewayCreem(props) {
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={t('Creem 设置')}>
<Text>
{t('Creem 介绍')}
<a href='https://creem.io' target='_blank' rel='noreferrer'>
Creem Official Site
</a>
<br />
</Text>
<Banner type='info' description={t('Creem Setting Tips')} />
<Form.Section text={sectionTitle}>
<Banner
type='info'
icon={<BookOpen size={16} />}
description={
<>
{t('Creem 介绍')}
<a href='https://creem.io' target='_blank' rel='noreferrer'>
Creem Official Site
</a>
<br />
{t('Creem Setting Tips')}
</>
}
style={{ marginBottom: 16 }}
/>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
@@ -281,7 +289,7 @@ export default function SettingsPaymentGatewayCreem(props) {
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='CreemWebhookSecret'
label={t('Webhook 密钥')}
label={t('Webhook 签名密钥')}
placeholder={t(
'用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示',
)}
@@ -291,7 +299,7 @@ export default function SettingsPaymentGatewayCreem(props) {
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Switch
field='CreemTestMode'
label={t('测试模式')}
label={t('沙盒模式')}
extraText={t('启用后将使用 Creem Test Mode')}
/>
</Col>
@@ -18,16 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import {
Banner,
Button,
Form,
Row,
Col,
Typography,
Spin,
} from '@douyinfe/semi-ui';
const { Text } = Typography;
import { Banner, Button, Form, Row, Col, Spin } from '@douyinfe/semi-ui';
import {
API,
removeTrailingSlash,
@@ -35,9 +26,11 @@ import {
showSuccess,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
import { BookOpen, TriangleAlert } from 'lucide-react';
export default function SettingsPaymentGateway(props) {
const { t } = useTranslation();
const sectionTitle = props.hideSectionTitle ? undefined : t('Stripe 设置');
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
StripeApiSecret: '',
@@ -165,42 +158,53 @@ export default function SettingsPaymentGateway(props) {
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={t('Stripe 设置')}>
<Text>
Stripe 密钥Webhook 等设置请
<a
href='https://dashboard.stripe.com/developers'
target='_blank'
rel='noreferrer'
>
点击此处
</a>
进行设置最好先在
<a
href='https://dashboard.stripe.com/test/developers'
target='_blank'
rel='noreferrer'
>
测试环境
</a>
进行测试
<br />
</Text>
<Form.Section text={sectionTitle}>
<Banner
type='info'
description={`Webhook 填:${props.options.ServerAddress ? removeTrailingSlash(props.options.ServerAddress) : t('网站地址')}/api/stripe/webhook`}
icon={<BookOpen size={16} />}
description={
<>
Stripe 密钥Webhook 等设置请
<a
href='https://dashboard.stripe.com/developers'
target='_blank'
rel='noreferrer'
>
点击此处
</a>
进行设置建议先在
<a
href='https://dashboard.stripe.com/test/developers'
target='_blank'
rel='noreferrer'
>
测试环境
</a>
完成联调
<br />
{t('回调地址')}
{props.options.ServerAddress
? removeTrailingSlash(props.options.ServerAddress)
: t('网站地址')}
/api/stripe/webhook
</>
}
style={{ marginBottom: 12 }}
/>
<Banner
type='warning'
description={`需要包含事件:checkout.session.completed 和 checkout.session.expired`}
icon={<TriangleAlert size={16} />}
description='需要包含事件:checkout.session.completed 和 checkout.session.expired'
style={{ marginBottom: 16 }}
/>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='StripeApiSecret'
label={t('API 密钥')}
placeholder={t(
'sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示',
placeholder={t('例如:sk_xxx 或 rk_xxx,留空表示保持当前不变')}
extraText={t(
'保存后不会回显,请填写当前环境对应的 Stripe API 密钥',
)}
type='password'
/>
@@ -209,7 +213,8 @@ export default function SettingsPaymentGateway(props) {
<Form.Input
field='StripeWebhookSecret'
label={t('Webhook 签名密钥')}
placeholder={t('whsec_xxx 的 Webhook 签名密钥,敏感信息不显示')}
placeholder={t('例如:whsec_xxx,留空表示保持当前不变')}
extraText={t('用于校验 Stripe Webhook 签名,保存后不会回显')}
type='password'
/>
</Col>
@@ -217,7 +222,8 @@ export default function SettingsPaymentGateway(props) {
<Form.Input
field='StripePriceId'
label={t('商品价格 ID')}
placeholder={t('price_xxx 的商品价格 ID,新建产品后可获得')}
placeholder={t('例如:price_xxx')}
extraText={t('在 Stripe 后台创建价格后获得')}
/>
</Col>
</Row>
@@ -231,6 +237,7 @@ export default function SettingsPaymentGateway(props) {
precision={2}
label={t('充值价格(x元/美金)')}
placeholder={t('例如:7,就是7元/美金')}
extraText={t('按 1 美元对应的站内价格填写')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
@@ -238,6 +245,7 @@ export default function SettingsPaymentGateway(props) {
field='StripeMinTopUp'
label={t('最低充值美元数量')}
placeholder={t('例如:2,就是最低充值2$')}
extraText={t('用户单次最少可充值的美元数量')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
@@ -31,13 +31,21 @@ import {
Input,
Space,
} from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../../helpers';
import {
API,
removeTrailingSlash,
showError,
showSuccess,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
import { BookOpen, TriangleAlert } from 'lucide-react';
const { Text } = Typography;
const toBoolean = (value) => value === true || value === 'true';
export default function SettingsPaymentGatewayWaffo(props) {
const { t } = useTranslation();
const sectionTitle = props.hideSectionTitle ? undefined : t('Waffo 设置');
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
WaffoEnabled: false,
@@ -55,7 +63,6 @@ export default function SettingsPaymentGatewayWaffo(props) {
WaffoNotifyUrl: '',
WaffoReturnUrl: '',
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);
const iconFileInputRef = useRef(null);
@@ -93,14 +100,14 @@ export default function SettingsPaymentGatewayWaffo(props) {
useEffect(() => {
if (props.options && formApiRef.current) {
const currentInputs = {
WaffoEnabled: props.options.WaffoEnabled === 'true' || props.options.WaffoEnabled === true,
WaffoEnabled: toBoolean(props.options.WaffoEnabled),
WaffoApiKey: props.options.WaffoApiKey || '',
WaffoPrivateKey: props.options.WaffoPrivateKey || '',
WaffoPublicCert: props.options.WaffoPublicCert || '',
WaffoSandboxPublicCert: props.options.WaffoSandboxPublicCert || '',
WaffoSandboxApiKey: props.options.WaffoSandboxApiKey || '',
WaffoSandboxPrivateKey: props.options.WaffoSandboxPrivateKey || '',
WaffoSandbox: props.options.WaffoSandbox === 'true',
WaffoSandbox: toBoolean(props.options.WaffoSandbox),
WaffoMerchantId: props.options.WaffoMerchantId || '',
WaffoCurrency: props.options.WaffoCurrency || 'USD',
WaffoUnitPrice: parseFloat(props.options.WaffoUnitPrice) || 1.0,
@@ -109,7 +116,6 @@ export default function SettingsPaymentGatewayWaffo(props) {
WaffoReturnUrl: props.options.WaffoReturnUrl || '',
};
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
formApiRef.current.setValues(currentInputs);
// 解析支付方式列表
@@ -149,15 +155,30 @@ export default function SettingsPaymentGatewayWaffo(props) {
options.push({ key: 'WaffoPrivateKey', value: inputs.WaffoPrivateKey });
}
options.push({ key: 'WaffoPublicCert', value: inputs.WaffoPublicCert || '' });
options.push({ key: 'WaffoSandboxPublicCert', value: inputs.WaffoSandboxPublicCert || '' });
options.push({
key: 'WaffoPublicCert',
value: inputs.WaffoPublicCert || '',
});
options.push({
key: 'WaffoSandboxPublicCert',
value: inputs.WaffoSandboxPublicCert || '',
});
if (inputs.WaffoSandboxApiKey && inputs.WaffoSandboxApiKey !== '') {
options.push({ key: 'WaffoSandboxApiKey', value: inputs.WaffoSandboxApiKey });
options.push({
key: 'WaffoSandboxApiKey',
value: inputs.WaffoSandboxApiKey,
});
}
if (inputs.WaffoSandboxPrivateKey && inputs.WaffoSandboxPrivateKey !== '') {
options.push({ key: 'WaffoSandboxPrivateKey', value: inputs.WaffoSandboxPrivateKey });
if (
inputs.WaffoSandboxPrivateKey &&
inputs.WaffoSandboxPrivateKey !== ''
) {
options.push({
key: 'WaffoSandboxPrivateKey',
value: inputs.WaffoSandboxPrivateKey,
});
}
options.push({
@@ -165,7 +186,10 @@ export default function SettingsPaymentGatewayWaffo(props) {
value: inputs.WaffoSandbox ? 'true' : 'false',
});
options.push({ key: 'WaffoMerchantId', value: inputs.WaffoMerchantId || '' });
options.push({
key: 'WaffoMerchantId',
value: inputs.WaffoMerchantId || '',
});
options.push({ key: 'WaffoCurrency', value: inputs.WaffoCurrency || '' });
options.push({
@@ -178,8 +202,14 @@ export default function SettingsPaymentGatewayWaffo(props) {
value: String(inputs.WaffoMinTopUp || 1),
});
options.push({ key: 'WaffoNotifyUrl', value: inputs.WaffoNotifyUrl || '' });
options.push({ key: 'WaffoReturnUrl', value: inputs.WaffoReturnUrl || '' });
options.push({
key: 'WaffoNotifyUrl',
value: inputs.WaffoNotifyUrl || '',
});
options.push({
key: 'WaffoReturnUrl',
value: inputs.WaffoReturnUrl || '',
});
// 保存支付方式列表
options.push({
@@ -205,8 +235,6 @@ export default function SettingsPaymentGatewayWaffo(props) {
});
} else {
showSuccess(t('更新成功'));
// 更新本地存储的原始值
setOriginInputs({ ...inputs });
props.refresh?.();
}
} catch (error) {
@@ -218,7 +246,12 @@ export default function SettingsPaymentGatewayWaffo(props) {
// 打开新增弹窗
const openAddPayMethodModal = () => {
setEditingPayMethodIndex(-1);
setPayMethodForm({ name: '', icon: '', payMethodType: '', payMethodName: '' });
setPayMethodForm({
name: '',
icon: '',
payMethodType: '',
payMethodName: '',
});
setPayMethodModalVisible(true);
};
@@ -324,19 +357,32 @@ export default function SettingsPaymentGatewayWaffo(props) {
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={t('Waffo 设置')}>
<Text>
{t('Waffo 是一个支付聚合平台,支持多种支付方式。')}
<a href='https://waffo.com' target='_blank' rel='noreferrer'>
Waffo Official Site
</a>
<br />
</Text>
<Form.Section text={sectionTitle}>
<Banner
type='info'
description={t(
'请在 Waffo 后台获取 API 密钥、商户 ID 以及 RSA 密钥对,并配置回调地址。',
)}
icon={<BookOpen size={16} />}
description={
<>
Waffo 密钥商户和支付方式等设置请
<a href='https://waffo.com' target='_blank' rel='noreferrer'>
点击此处
</a>
进行配置切换沙盒模式时请同步填写对应环境的密钥
<br />
{t('回调地址')}
{props.options.ServerAddress
? removeTrailingSlash(props.options.ServerAddress)
: t('网站地址')}
/api/waffo/webhook
</>
}
style={{ marginBottom: 12 }}
/>
<Banner
type='warning'
icon={<TriangleAlert size={16} />}
description={t('请确认商户和所选环境密钥一致。')}
style={{ marginBottom: 16 }}
/>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
@@ -356,161 +402,188 @@ export default function SettingsPaymentGatewayWaffo(props) {
size='default'
checkedText=''
uncheckedText=''
extraText={t('启用后将使用 Waffo 沙盒环境')}
extraText={t('用于切换当前下单和回调校验所使用的环境')}
/>
</Col>
</Row>
<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='WaffoApiKey'
label={t('API 密钥 (生产)')}
placeholder={t('生产环境 Waffo API 密钥')}
type='password'
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='WaffoSandboxApiKey'
label={t('API 密钥 (沙盒)')}
placeholder={t('沙盒环境 Waffo API 密钥')}
type='password'
/>
</Col>
</Row>
<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}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoMerchantId'
label={t('商户 ID')}
placeholder={t('Waffo 商户 ID')}
placeholder={t('例如:MER_xxx')}
extraText={t('当前环境共用同一商户 ID')}
/>
</Col>
</Row>
<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}>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoApiKey'
label={t('API 密钥(生产环境)')}
placeholder={t(
'填写后覆盖当前生产环境 API 密钥,留空表示保持当前不变',
)}
extraText={t('保存后不会回显,请填写生产环境对应的 API 密钥')}
type='password'
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.TextArea
field='WaffoPrivateKey'
label={t('RSA 私钥 (生产)')}
placeholder={t('生产环境 RSA 私钥 Base64 (PKCS#8 DER)')}
label={t('API 私钥(生产环境)')}
placeholder={t(
'填写后覆盖当前生产环境私钥,留空表示保持当前不变',
)}
extraText={t('保存后不会回显,请填写生产环境对应的 API 私钥')}
type='password'
autosize={{ minRows: 3, maxRows: 6 }}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='WaffoSandboxPrivateKey'
label={t('RSA 私钥 (沙盒)')}
placeholder={t('沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)')}
type='password'
autosize={{ minRows: 3, maxRows: 6 }}
/>
</Col>
</Row>
<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}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.TextArea
field='WaffoPublicCert'
label={t('Waffo 公钥 (生产)')}
placeholder={t('生产环境 Waffo 公钥 Base64 (X.509 DER)')}
type='password'
autosize={{ minRows: 3, maxRows: 6 }}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='WaffoSandboxPublicCert'
label={t('Waffo 公钥 (沙盒)')}
placeholder={t('沙盒环境 Waffo 公钥 Base64 (X.509 DER)')}
label={t('Waffo 公钥(生产环境)')}
placeholder={t(
'填写生产环境 Waffo 公钥,Base64 或 PEM 内容均可',
)}
extraText={t('用于校验生产环境的 Waffo 回调签名')}
type='password'
autosize={{ minRows: 3, maxRows: 6 }}
/>
</Col>
</Row>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoSandboxApiKey'
label={t('API 密钥(测试环境)')}
placeholder={t(
'填写后覆盖当前测试环境 API 密钥,留空表示保持当前不变',
)}
extraText={t('保存后不会回显,请填写测试环境对应的 API 密钥')}
type='password'
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.TextArea
field='WaffoSandboxPrivateKey'
label={t('API 私钥(测试环境)')}
placeholder={t(
'填写后覆盖当前测试环境私钥,留空表示保持当前不变',
)}
extraText={t('保存后不会回显,请填写测试环境对应的 API 私钥')}
type='password'
autosize={{ minRows: 3, maxRows: 6 }}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.TextArea
field='WaffoSandboxPublicCert'
label={t('Waffo 公钥(测试环境)')}
placeholder={t(
'填写测试环境 Waffo 公钥,Base64 或 PEM 内容均可',
)}
extraText={t('用于校验测试环境的 Waffo 回调签名')}
type='password'
autosize={{ minRows: 3, maxRows: 6 }}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoCurrency'
label={t('货币')}
placeholder='USD'
extraText={t('Waffo 当前使用 USD 结算')}
disabled
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.InputNumber
field='WaffoUnitPrice'
label={t('单价 (USD)')}
placeholder='1.0'
precision={2}
label={t('充值价格(x元/美金)')}
placeholder={t('例如:7,就是7元/美金')}
extraText={t('按 1 美元对应的站内价格填写')}
min={0}
step={0.1}
extraText={t('每个充值单位对应的 USD 金额,默认 1.0')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.InputNumber
field='WaffoMinTopUp'
label={t('最低充值数量')}
placeholder='1'
label={t('最低充值美元数量')}
placeholder={t('例如:2,就是最低充值2$')}
extraText={t('用户单次最少可充值的美元数量')}
min={1}
step={1}
extraText={t('Waffo 充值的最低数量,默认 1')}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='WaffoNotifyUrl'
label={t('回调通知地址')}
placeholder={t('例如 https://example.com/api/waffo/webhook')}
extraText={t('留空则自动使用 服务器地址 + /api/waffo/webhook')}
label={t('回调地址')}
placeholder={t('例如https://example.com/api/waffo/webhook')}
extraText={t('留空则自动使用当前站点的默认回调地址')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='WaffoReturnUrl'
label={t('支付返回地址')}
placeholder={t('例如 https://example.com/console/topup')}
extraText={t('支付完成后用户跳转的页面,留空则自动使用 服务器地址 + /console/topup')}
placeholder={t('例如https://example.com/console/topup')}
extraText={t('留空则自动使用当前站点的默认充值页地址')}
/>
</Col>
</Row>
</Form.Section>
<Form.Section text={t('支付方式设置')}>
<Text type='secondary'>
{t(
'这里配置 Waffo 下展示给用户的 Card、Apple Pay、Google Pay 等子支付方式。',
)}
</Text>
<div style={{ marginTop: 12, marginBottom: 12 }}>
<Button onClick={openAddPayMethodModal}>{t('新增支付方式')}</Button>
</div>
<Table
columns={payMethodColumns}
dataSource={waffoPayMethods}
rowKey={(record, index) => index}
pagination={false}
size='small'
empty={
<Text type='tertiary'>{t('暂无支付方式,点击上方按钮新增')}</Text>
}
/>
<Button onClick={submitWaffoSetting} style={{ marginTop: 16 }}>
{t('更新 Waffo 设置')}
</Button>
</Form.Section>
</Form>
{/* 支付方式配置区块(独立于 Form,使用独立状态管理) */}
<div style={{ marginTop: 24 }}>
<Typography.Title heading={6} style={{ marginBottom: 8 }}>{t('支付方式')}</Typography.Title>
<Text type='secondary'>
{t('配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。')}
</Text>
<div style={{ marginTop: 12, marginBottom: 12 }}>
<Button onClick={openAddPayMethodModal}>
{t('新增支付方式')}
</Button>
</div>
<Table
columns={payMethodColumns}
dataSource={waffoPayMethods}
rowKey={(record, index) => index}
pagination={false}
size='small'
empty={<Text type='tertiary'>{t('暂无支付方式,点击上方按钮新增')}</Text>}
/>
<Button onClick={submitWaffoSetting} style={{ marginTop: 16 }}>
{t('更新 Waffo 设置')}
</Button>
</div>
{/* 新增/编辑支付方式弹窗 */}
<Modal
title={editingPayMethodIndex === -1 ? t('新增支付方式') : t('编辑支付方式')}
title={
editingPayMethodIndex === -1 ? t('新增支付方式') : t('编辑支付方式')
}
visible={payMethodModalVisible}
onOk={handlePayMethodModalOk}
onCancel={() => setPayMethodModalVisible(false)}
@@ -521,14 +594,22 @@ export default function SettingsPaymentGatewayWaffo(props) {
<div>
<div style={{ marginBottom: 4 }}>
<Text strong>{t('显示名称')}</Text>
<span style={{ color: 'var(--semi-color-danger)', marginLeft: 4 }}>*</span>
<span
style={{ color: 'var(--semi-color-danger)', marginLeft: 4 }}
>
*
</span>
</div>
<Input
value={payMethodForm.name}
onChange={(val) => setPayMethodForm({ ...payMethodForm, name: val })}
onChange={(val) =>
setPayMethodForm({ ...payMethodForm, name: val })
}
placeholder={t('例如:Credit Card')}
/>
<Text type='tertiary' size='small'>{t('用户在充值页面看到的支付方式名称,例如:Credit Card')}</Text>
<Text type='tertiary' size='small'>
{t('用户在充值页面看到的支付方式名称,例如:Credit Card')}
</Text>
</div>
<div>
<div style={{ marginBottom: 4 }}>
@@ -574,32 +655,44 @@ export default function SettingsPaymentGatewayWaffo(props) {
)}
</Space>
<div>
<Text type='tertiary' size='small'>{t('上传 PNG/JPG/SVG 图片,建议尺寸 ≤ 128×128px')}</Text>
<Text type='tertiary' size='small'>
{t('上传 PNG/JPG/SVG 图片,建议尺寸 ≤ 128×128px')}
</Text>
</div>
</div>
<div>
<div style={{ marginBottom: 4 }}>
<Text strong>{t('Pay Method Type')}</Text>
<Text strong>{t('支付方式类型')}</Text>
</div>
<Input
value={payMethodForm.payMethodType}
onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodType: val })}
onChange={(val) =>
setPayMethodForm({ ...payMethodForm, payMethodType: val })
}
placeholder='CREDITCARD,DEBITCARD'
maxLength={64}
/>
<Text type='tertiary' size='small'>{t('Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)')}</Text>
<Text type='tertiary' size='small'>
{t(
'Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)',
)}
</Text>
</div>
<div>
<div style={{ marginBottom: 4 }}>
<Text strong>{t('Pay Method Name')}</Text>
<Text strong>{t('支付方式名称')}</Text>
</div>
<Input
value={payMethodForm.payMethodName}
onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodName: val })}
onChange={(val) =>
setPayMethodForm({ ...payMethodForm, payMethodName: val })
}
placeholder={t('可空')}
maxLength={64}
/>
<Text type='tertiary' size='small'>{t('Waffo API 参数,可空(最多64位)')}</Text>
<Text type='tertiary' size='small'>
{t('Waffo API 参数,可空(最多64位)')}
</Text>
</div>
</div>
</Modal>
@@ -0,0 +1,411 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
API,
removeTrailingSlash,
showError,
showSuccess,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
import { BookOpen, TriangleAlert } from 'lucide-react';
const defaultInputs = {
WaffoPancakeEnabled: false,
WaffoPancakeSandbox: false,
WaffoPancakeMerchantID: '',
WaffoPancakePrivateKey: '',
WaffoPancakeWebhookPublicKey: '',
WaffoPancakeWebhookTestKey: '',
WaffoPancakeStoreID: '',
WaffoPancakeProductID: '',
WaffoPancakeReturnURL: '',
WaffoPancakeCurrency: 'USD',
WaffoPancakeUnitPrice: 1.0,
WaffoPancakeMinTopUp: 1,
};
const toBoolean = (value) => value === true || value === 'true';
export default function SettingsPaymentGatewayWaffoPancake(props) {
const { t } = useTranslation();
const sectionTitle = props.hideSectionTitle
? undefined
: t('Waffo Pancake 设置');
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState(defaultInputs);
const formApiRef = useRef(null);
useEffect(() => {
if (!props.options || !formApiRef.current) return;
const currentInputs = {
WaffoPancakeEnabled: toBoolean(props.options.WaffoPancakeEnabled),
WaffoPancakeSandbox: toBoolean(props.options.WaffoPancakeSandbox),
WaffoPancakeMerchantID: props.options.WaffoPancakeMerchantID || '',
WaffoPancakePrivateKey: props.options.WaffoPancakePrivateKey || '',
WaffoPancakeWebhookPublicKey:
props.options.WaffoPancakeWebhookPublicKey || '',
WaffoPancakeWebhookTestKey:
props.options.WaffoPancakeWebhookTestKey || '',
WaffoPancakeStoreID: props.options.WaffoPancakeStoreID || '',
WaffoPancakeProductID: props.options.WaffoPancakeProductID || '',
WaffoPancakeReturnURL: props.options.WaffoPancakeReturnURL || '',
WaffoPancakeCurrency: props.options.WaffoPancakeCurrency || 'USD',
WaffoPancakeUnitPrice:
props.options.WaffoPancakeUnitPrice !== undefined
? parseFloat(props.options.WaffoPancakeUnitPrice)
: 1.0,
WaffoPancakeMinTopUp:
props.options.WaffoPancakeMinTopUp !== undefined
? parseFloat(props.options.WaffoPancakeMinTopUp)
: 1,
};
setInputs(currentInputs);
formApiRef.current.setValues(currentInputs);
}, [props.options]);
const handleFormChange = (values) => {
setInputs(values);
};
const submitWaffoPancakeSetting = async () => {
const values = {
...inputs,
...(formApiRef.current?.getValues?.() || {}),
};
values.WaffoPancakeEnabled = toBoolean(values.WaffoPancakeEnabled);
values.WaffoPancakeSandbox = toBoolean(values.WaffoPancakeSandbox);
const currentWebhookField = values.WaffoPancakeSandbox
? 'WaffoPancakeWebhookTestKey'
: 'WaffoPancakeWebhookPublicKey';
const currentWebhookLabel = values.WaffoPancakeSandbox
? t('Webhook 公钥(测试环境)')
: t('Webhook 公钥(生产环境)');
if (values.WaffoPancakeEnabled && !values.WaffoPancakeMerchantID.trim()) {
showError(t('请输入商户 ID'));
return;
}
if (values.WaffoPancakeEnabled && !values.WaffoPancakeStoreID.trim()) {
showError(t('请输入 Store ID'));
return;
}
if (values.WaffoPancakeEnabled && !values.WaffoPancakeProductID.trim()) {
showError(t('请输入 Product ID'));
return;
}
if (
values.WaffoPancakeEnabled &&
!String(values[currentWebhookField] || '').trim()
) {
showError(currentWebhookLabel);
return;
}
if (
values.WaffoPancakeEnabled &&
Number(values.WaffoPancakeUnitPrice) <= 0
) {
showError(t('充值价格必须大于 0'));
return;
}
if (values.WaffoPancakeEnabled && Number(values.WaffoPancakeMinTopUp) < 1) {
showError(t('最低充值美元数量必须大于 0'));
return;
}
setLoading(true);
try {
const options = [
{
key: 'WaffoPancakeEnabled',
value: values.WaffoPancakeEnabled ? 'true' : 'false',
},
{
key: 'WaffoPancakeSandbox',
value: values.WaffoPancakeSandbox ? 'true' : 'false',
},
{
key: 'WaffoPancakeMerchantID',
value: values.WaffoPancakeMerchantID || '',
},
{
key: 'WaffoPancakeStoreID',
value: values.WaffoPancakeStoreID || '',
},
{
key: 'WaffoPancakeProductID',
value: values.WaffoPancakeProductID || '',
},
{
key: 'WaffoPancakeReturnURL',
value: removeTrailingSlash(values.WaffoPancakeReturnURL || ''),
},
{
key: 'WaffoPancakeCurrency',
value: values.WaffoPancakeCurrency || 'USD',
},
{
key: 'WaffoPancakeUnitPrice',
value: String(values.WaffoPancakeUnitPrice),
},
{
key: 'WaffoPancakeMinTopUp',
value: String(values.WaffoPancakeMinTopUp),
},
];
if ((values.WaffoPancakePrivateKey || '').trim()) {
options.push({
key: 'WaffoPancakePrivateKey',
value: values.WaffoPancakePrivateKey,
});
}
if ((values.WaffoPancakeWebhookPublicKey || '').trim()) {
options.push({
key: 'WaffoPancakeWebhookPublicKey',
value: values.WaffoPancakeWebhookPublicKey,
});
}
if ((values.WaffoPancakeWebhookTestKey || '').trim()) {
options.push({
key: 'WaffoPancakeWebhookTestKey',
value: values.WaffoPancakeWebhookTestKey,
});
}
const results = await Promise.all(
options.map((opt) =>
API.put('/api/option/', {
key: opt.key,
value: opt.value,
}),
),
);
const errorResults = results.filter((res) => !res.data.success);
if (errorResults.length > 0) {
errorResults.forEach((res) => showError(res.data.message));
return;
}
showSuccess(t('更新成功'));
props.refresh?.();
} catch (error) {
showError(t('更新失败'));
} finally {
setLoading(false);
}
};
return (
<Spin spinning={loading}>
<Form
initValues={inputs}
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={sectionTitle}>
<Banner
type='info'
icon={<BookOpen size={16} />}
description={
<>
Waffo Pancake 的商户商品和签名密钥请
<a
href='https://docs.waffo.ai'
target='_blank'
rel='noreferrer'
>
点击此处
</a>
获取建议先在测试环境完成联调
<br />
{t('回调地址')}
{props.options.ServerAddress
? removeTrailingSlash(props.options.ServerAddress)
: t('网站地址')}
/api/waffo-pancake/webhook
</>
}
style={{ marginBottom: 12 }}
/>
<Banner
type='warning'
icon={<TriangleAlert size={16} />}
description={t(
'请确认 Merchant、Store、Product 和所选环境密钥一致。',
)}
style={{ marginBottom: 16 }}
/>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field='WaffoPancakeEnabled'
label={t('启用 Waffo Pancake')}
checkedText=''
uncheckedText=''
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field='WaffoPancakeSandbox'
label={t('沙盒模式')}
checkedText=''
uncheckedText=''
extraText={t('用于切换当前下单和回调校验所使用的环境')}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoPancakeCurrency'
label={t('货币')}
placeholder='USD'
extraText={t('默认使用 USD 结算')}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoPancakeMerchantID'
label={t('商户 ID')}
placeholder={t('例如:MER_xxx')}
extraText={t('请填写当前环境对应的商户 ID')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoPancakeStoreID'
label={t('Store ID')}
placeholder={t('例如:STO_xxx')}
extraText={t('请填写当前环境对应的 Store ID')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WaffoPancakeProductID'
label={t('Product ID')}
placeholder={t('例如:PROD_xxx')}
extraText={t('请填写当前环境对应的 Product ID')}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='WaffoPancakePrivateKey'
label={t('API 私钥')}
placeholder={t('填写后覆盖当前私钥,留空表示保持当前不变')}
extraText={t('保存后不会回显,请填写当前环境对应的 API 私钥')}
type='password'
autosize={{ minRows: 4, maxRows: 8 }}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='WaffoPancakeReturnURL'
label={t('支付返回地址')}
placeholder={t('例如:https://example.com/console/topup')}
extraText={t('留空则自动使用当前站点的默认充值页地址')}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='WaffoPancakeWebhookPublicKey'
label={t('Webhook 公钥(生产环境)')}
placeholder={t(
'填写后覆盖当前生产环境 Webhook 公钥,留空表示保持当前不变',
)}
extraText={t('用于校验生产环境的 Waffo Pancake Webhook 签名')}
type='password'
autosize={{ minRows: 4, maxRows: 8 }}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='WaffoPancakeWebhookTestKey'
label={t('Webhook 公钥(测试环境)')}
placeholder={t(
'填写后覆盖当前测试环境 Webhook 公钥,留空表示保持当前不变',
)}
extraText={t('用于校验测试环境的 Waffo Pancake Webhook 签名')}
type='password'
autosize={{ minRows: 4, maxRows: 8 }}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
field='WaffoPancakeUnitPrice'
precision={2}
label={t('充值价格(x元/美金)')}
placeholder={t('例如:7,就是7元/美金')}
extraText={t('按 1 美元对应的站内价格填写')}
min={0}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
field='WaffoPancakeMinTopUp'
label={t('最低充值美元数量')}
placeholder={t('例如:2,就是最低充值2$')}
extraText={t('用户单次最少可充值的美元数量')}
min={1}
/>
</Col>
</Row>
<Button onClick={submitWaffoPancakeSetting}>
{t('更新 Waffo Pancake 设置')}
</Button>
</Form.Section>
</Form>
</Spin>
);
}