Merge pull request #3293 from zhongyuanzhao-alt/ft-waffo-payment-zzy20260317
feat(waffo): Waffo payment gateway integration
This commit is contained in:
BIN
web/public/pay-apple.png
vendored
Normal file
BIN
web/public/pay-apple.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
web/public/pay-card.png
vendored
Normal file
BIN
web/public/pay-card.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
web/public/pay-google.png
vendored
Normal file
BIN
web/public/pay-google.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -23,6 +23,7 @@ import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralP
|
||||
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 { API, showError, toBoolean } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -66,7 +67,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析TopupGroupRatio出错:', error);
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -78,7 +78,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析AmountOptions出错:', error);
|
||||
newInputs['AmountOptions'] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -90,7 +89,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析AmountDiscount出错:', error);
|
||||
newInputs['AmountDiscount'] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -146,6 +144,9 @@ const PaymentSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayWaffo options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -87,6 +87,9 @@ const RechargeCard = ({
|
||||
statusLoading,
|
||||
topupInfo,
|
||||
onOpenHistory,
|
||||
enableWaffoTopUp,
|
||||
waffoTopUp,
|
||||
waffoPayMethods,
|
||||
subscriptionLoading = false,
|
||||
subscriptionPlans = [],
|
||||
billingPreference,
|
||||
@@ -224,19 +227,19 @@ const RechargeCard = ({
|
||||
<div className='py-8 flex justify-center'>
|
||||
<Spin size='large' />
|
||||
</div>
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? (
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
|
||||
<Form
|
||||
getFormApi={(api) => (onlineFormApiRef.current = api)}
|
||||
initValues={{ topUpCount: topUpCount }}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
|
||||
<Row gutter={12}>
|
||||
<Col xs={24} sm={24} md={24} lg={10} xl={10}>
|
||||
<Form.InputNumber
|
||||
field='topUpCount'
|
||||
label={t('充值数量')}
|
||||
disabled={!enableOnlineTopUp && !enableStripeTopUp}
|
||||
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWaffoTopUp}
|
||||
placeholder={
|
||||
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
|
||||
}
|
||||
@@ -288,11 +291,11 @@ 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('选择支付方式')}>
|
||||
{payMethods && payMethods.length > 0 ? (
|
||||
<Space wrap>
|
||||
{payMethods.map((payMethod) => {
|
||||
{payMethods.filter(m => m.type !== 'waffo').map((payMethod) => {
|
||||
const minTopupVal = Number(payMethod.min_topup) || 0;
|
||||
const isStripe = payMethod.type === 'stripe';
|
||||
const disabled =
|
||||
@@ -352,17 +355,13 @@ const RechargeCard = ({
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
) : (
|
||||
<div className='text-gray-500 text-sm p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300'>
|
||||
{t('暂无可用的支付方式,请联系管理员配置')}
|
||||
</div>
|
||||
)}
|
||||
</Form.Slot>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
|
||||
<Form.Slot
|
||||
label={
|
||||
<div className='flex items-center gap-2'>
|
||||
@@ -483,6 +482,46 @@ 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 充值')}>
|
||||
|
||||
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useContext, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal';
|
||||
|
||||
const TopUp = () => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState] = useContext(StatusContext);
|
||||
|
||||
@@ -69,6 +71,11 @@ const TopUp = () => {
|
||||
const [creemOpen, setCreemOpen] = useState(false);
|
||||
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
|
||||
|
||||
// Waffo 相关状态
|
||||
const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
|
||||
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
|
||||
const [waffoMinTopUp, setWaffoMinTopUp] = useState(1);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [payWay, setPayWay] = useState('');
|
||||
@@ -256,7 +263,6 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setOpen(false);
|
||||
@@ -302,7 +308,6 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setCreemOpen(false);
|
||||
@@ -310,6 +315,37 @@ 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('支付请求失败'));
|
||||
}
|
||||
} else {
|
||||
showError(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setPaymentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processCreemCallback = (data) => {
|
||||
// 与 Stripe 保持一致的实现方式
|
||||
window.open(data.checkout_url, '_blank');
|
||||
@@ -449,17 +485,21 @@ const TopUp = () => {
|
||||
? data.min_topup
|
||||
: enableStripeTopUp
|
||||
? data.stripe_min_topup
|
||||
: 1;
|
||||
: data.enable_waffo_topup
|
||||
? data.waffo_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);
|
||||
setMinTopUp(minTopUpValue);
|
||||
setTopUpCount(minTopUpValue);
|
||||
|
||||
// 设置 Creem 产品
|
||||
try {
|
||||
console.log(' data is ?', data);
|
||||
console.log(' creem products is ?', data.creem_products);
|
||||
const products = JSON.parse(data.creem_products || '[]');
|
||||
setCreemProducts(products);
|
||||
} catch (e) {
|
||||
@@ -474,7 +514,6 @@ const TopUp = () => {
|
||||
// 初始化显示实付金额
|
||||
getAmount(minTopUpValue);
|
||||
} catch (e) {
|
||||
console.log('解析支付方式失败:', e);
|
||||
setPayMethods([]);
|
||||
}
|
||||
|
||||
@@ -487,10 +526,10 @@ const TopUp = () => {
|
||||
setPresetAmounts(customPresets);
|
||||
}
|
||||
} else {
|
||||
console.error('获取充值配置失败:', data);
|
||||
showError(data || t('获取充值配置失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取充值配置异常:', error);
|
||||
showError(t('获取充值配置异常'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -531,6 +570,15 @@ const TopUp = () => {
|
||||
showSuccess(t('邀请链接已复制到剪切板'));
|
||||
};
|
||||
|
||||
// URL 参数自动打开账单弹窗(支付回跳时触发)
|
||||
useEffect(() => {
|
||||
if (searchParams.get('show_history') === 'true') {
|
||||
setOpenHistory(true);
|
||||
searchParams.delete('show_history');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 始终获取最新用户数据,确保余额等统计信息准确
|
||||
getUserQuota().then();
|
||||
@@ -587,7 +635,7 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// amount fetch failed silently
|
||||
}
|
||||
setAmountLoading(false);
|
||||
};
|
||||
@@ -613,7 +661,7 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// amount fetch failed silently
|
||||
} finally {
|
||||
setAmountLoading(false);
|
||||
}
|
||||
@@ -740,6 +788,9 @@ const TopUp = () => {
|
||||
enableCreemTopUp={enableCreemTopUp}
|
||||
creemProducts={creemProducts}
|
||||
creemPreTopUp={creemPreTopUp}
|
||||
enableWaffoTopUp={enableWaffoTopUp}
|
||||
waffoTopUp={waffoTopUp}
|
||||
waffoPayMethods={waffoPayMethods}
|
||||
presetAmounts={presetAmounts}
|
||||
selectedPreset={selectedPreset}
|
||||
selectPresetAmount={selectPresetAmount}
|
||||
|
||||
@@ -37,13 +37,13 @@ import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { API, timestamp2string } from '../../../helpers';
|
||||
import { isAdmin } from '../../../helpers/utils';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 状态映射配置
|
||||
const STATUS_CONFIG = {
|
||||
success: { type: 'success', key: '成功' },
|
||||
pending: { type: 'warning', key: '待支付' },
|
||||
failed: { type: 'danger', key: '失败' },
|
||||
expired: { type: 'danger', key: '已过期' },
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ const STATUS_CONFIG = {
|
||||
const PAYMENT_METHOD_MAP = {
|
||||
stripe: 'Stripe',
|
||||
creem: 'Creem',
|
||||
waffo: 'Waffo',
|
||||
alipay: '支付宝',
|
||||
wxpay: '微信',
|
||||
};
|
||||
@@ -62,7 +63,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const loadTopups = async (currentPage, currentPageSize) => {
|
||||
@@ -82,7 +82,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
Toast.error({ content: message || t('加载失败') });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load topups error:', error);
|
||||
Toast.error({ content: t('加载账单失败') });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -214,17 +213,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => {
|
||||
if (record.status !== 'pending') return null;
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
const actions = [];
|
||||
if (record.status === 'pending') {
|
||||
actions.push(
|
||||
<Button
|
||||
key="complete"
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return actions.length > 0 ? <>{actions}</> : null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
10
web/src/i18n/locales/en.json
vendored
10
web/src/i18n/locales/en.json
vendored
@@ -3216,6 +3216,16 @@
|
||||
"默认测试模型": "Default Test Model",
|
||||
"默认用户消息": "Default User Message",
|
||||
"默认补全倍率": "Default completion ratio",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Notice: Endpoint mapping is for Model Marketplace display only and does not affect real model invocation. To configure real invocation, please go to Channel Management.",
|
||||
"购买订阅获得模型额度/次数": "Purchase a subscription to get model quota/usage",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Production RSA private key Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Sandbox RSA private key Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Production Waffo public key Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Sandbox Waffo public key Base64 (X.509 DER)",
|
||||
"支付方式类型": "Pay Method Type",
|
||||
"支付方式名称": "Pay Method Name",
|
||||
"获取充值配置失败": "Failed to get topup configuration",
|
||||
"获取充值配置异常": "Topup configuration error",
|
||||
"分组相关设置": "Group Related Settings",
|
||||
"保存分组相关设置": "Save Group Related Settings",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "This page only shows models without base pricing. After saving, configured models will be removed from this list automatically.",
|
||||
|
||||
10
web/src/i18n/locales/fr.json
vendored
10
web/src/i18n/locales/fr.json
vendored
@@ -3160,6 +3160,16 @@
|
||||
"默认测试模型": "Modèle de test par défaut",
|
||||
"默认用户消息": "Bonjour",
|
||||
"默认补全倍率": "Taux de complétion par défaut",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Remarque : la correspondance des endpoints sert uniquement à l'affichage dans la place de marché des modèles et n'affecte pas l'invocation réelle. Pour configurer l'invocation réelle, veuillez aller dans « Gestion des canaux ».",
|
||||
"购买订阅获得模型额度/次数": "Acheter un abonnement pour obtenir des quotas/usages de modèles",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de production",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de sandbox",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de production",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de sandbox",
|
||||
"支付方式类型": "Type de méthode de paiement",
|
||||
"支付方式名称": "Nom de méthode de paiement",
|
||||
"获取充值配置失败": "Échec de la récupération de la configuration de recharge",
|
||||
"获取充值配置异常": "Erreur de configuration de recharge",
|
||||
"分组相关设置": "Paramètres liés aux groupes",
|
||||
"保存分组相关设置": "Enregistrer les paramètres liés aux groupes",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "Cette page n'affiche que les modèles sans prix ou ratio de base. Après enregistrement, ils seront retirés automatiquement de cette liste.",
|
||||
|
||||
10
web/src/i18n/locales/ja.json
vendored
10
web/src/i18n/locales/ja.json
vendored
@@ -3141,6 +3141,16 @@
|
||||
"默认测试模型": "デフォルトテストモデル",
|
||||
"默认用户消息": "こんにちは",
|
||||
"默认补全倍率": "デフォルト補完倍率",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "注意: エンドポイントマッピングは「モデル広場」での表示専用で、実際の呼び出しには影響しません。実際の呼び出し設定は「チャネル管理」で行ってください。",
|
||||
"购买订阅获得模型额度/次数": "サブスクリプション購入でモデルのクォータ/回数を取得",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "本番環境 RSA 秘密鍵 Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "サンドボックス RSA 秘密鍵 Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "本番環境 Waffo 公開鍵 Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "サンドボックス Waffo 公開鍵 Base64 (X.509 DER)",
|
||||
"支付方式类型": "決済方法タイプ",
|
||||
"支付方式名称": "決済方法名",
|
||||
"获取充值配置失败": "チャージ設定の取得に失敗しました",
|
||||
"获取充值配置异常": "チャージ設定エラー",
|
||||
"分组相关设置": "グループ関連設定",
|
||||
"保存分组相关设置": "グループ関連設定を保存",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "このページには価格または基本倍率が未設定のモデルのみ表示され、設定後は一覧から自動的に消えます。",
|
||||
|
||||
12
web/src/i18n/locales/ru.json
vendored
12
web/src/i18n/locales/ru.json
vendored
@@ -3173,7 +3173,17 @@
|
||||
"默认折叠侧边栏": "Сворачивать боковую панель по умолчанию",
|
||||
"默认测试模型": "Модель для тестирования по умолчанию",
|
||||
"默认用户消息": "Здравствуйте",
|
||||
"默认补全倍率": "Default completion ratio",
|
||||
"默认补全倍率": "Коэффициент завершения по умолчанию",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление эндпоинтов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
|
||||
"购买订阅获得模型额度/次数": "Купите подписку, чтобы получить лимит/количество использования моделей",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) производственной среды",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) песочницы",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) производственной среды",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) песочницы",
|
||||
"支付方式类型": "Тип метода оплаты",
|
||||
"支付方式名称": "Название метода оплаты",
|
||||
"获取充值配置失败": "Не удалось получить конфигурацию пополнения",
|
||||
"获取充值配置异常": "Ошибка конфигурации пополнения",
|
||||
"分组相关设置": "Настройки, связанные с группами",
|
||||
"保存分组相关设置": "Сохранить настройки, связанные с группами",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "На этой странице показаны только модели без цены или базового коэффициента. После сохранения они будут автоматически удалены из списка.",
|
||||
|
||||
10
web/src/i18n/locales/vi.json
vendored
10
web/src/i18n/locales/vi.json
vendored
@@ -3712,6 +3712,16 @@
|
||||
"默认测试模型": "Mô hình kiểm tra mặc định",
|
||||
"默认用户消息": "Xin chào",
|
||||
"默认补全倍率": "Tỷ lệ hoàn thành mặc định",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Lưu ý: Ánh xạ endpoint chỉ dùng để hiển thị trong \"Chợ mô hình\" và không ảnh hưởng đến việc gọi thực tế. Để cấu hình gọi thực tế, vui lòng vào \"Quản lý kênh\".",
|
||||
"购买订阅获得模型额度/次数": "Mua đăng ký để nhận hạn mức/lượt dùng mô hình",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sản xuất",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sandbox",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sản xuất",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sandbox",
|
||||
"支付方式类型": "Loại phương thức thanh toán",
|
||||
"支付方式名称": "Tên phương thức thanh toán",
|
||||
"获取充值配置失败": "Không thể lấy cấu hình nạp tiền",
|
||||
"获取充值配置异常": "Lỗi cấu hình nạp tiền",
|
||||
"分组相关设置": "Cài đặt liên quan đến nhóm",
|
||||
"保存分组相关设置": "Lưu cài đặt liên quan đến nhóm",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "Trang này chỉ hiển thị các mô hình chưa thiết lập giá hoặc tỷ lệ cơ bản. Sau khi lưu, chúng sẽ tự động biến mất khỏi danh sách.",
|
||||
|
||||
10
web/src/i18n/locales/zh-TW.json
vendored
10
web/src/i18n/locales/zh-TW.json
vendored
@@ -2900,6 +2900,16 @@
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "提示:端點映射僅用於模型廣場展示,不會影響模型真實呼叫。如需配置真實呼叫,請前往「管道管理」。",
|
||||
"购买订阅获得模型额度/次数": "購買訂閱取得模型額度/次數",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "正式環境 RSA 私鑰 Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "沙盒環境 RSA 私鑰 Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "正式環境 Waffo 公鑰 Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "沙盒環境 Waffo 公鑰 Base64 (X.509 DER)",
|
||||
"支付方式类型": "付款方式類型",
|
||||
"支付方式名称": "付款方式名稱",
|
||||
"获取充值配置失败": "取得儲值設定失敗",
|
||||
"获取充值配置异常": "儲值設定異常",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "模型價格 {{price}}",
|
||||
|
||||
608
web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx
Normal file
608
web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx
Normal file
@@ -0,0 +1,608 @@
|
||||
/*
|
||||
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, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Spin,
|
||||
Table,
|
||||
Modal,
|
||||
Input,
|
||||
Space,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function SettingsPaymentGatewayWaffo(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
WaffoEnabled: false,
|
||||
WaffoApiKey: '',
|
||||
WaffoPrivateKey: '',
|
||||
WaffoPublicCert: '',
|
||||
WaffoSandboxPublicCert: '',
|
||||
WaffoSandboxApiKey: '',
|
||||
WaffoSandboxPrivateKey: '',
|
||||
WaffoSandbox: false,
|
||||
WaffoMerchantId: '',
|
||||
WaffoCurrency: 'USD',
|
||||
WaffoUnitPrice: 1.0,
|
||||
WaffoMinTopUp: 1,
|
||||
WaffoNotifyUrl: '',
|
||||
WaffoReturnUrl: '',
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
const formApiRef = useRef(null);
|
||||
const iconFileInputRef = useRef(null);
|
||||
|
||||
const handleIconFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const MAX_ICON_SIZE = 100 * 1024; // 100 KB
|
||||
if (file.size > MAX_ICON_SIZE) {
|
||||
showError(t('图标文件不能超过 100KB,请压缩后重新上传'));
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setPayMethodForm((prev) => ({ ...prev, icon: event.target.result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
// 支付方式列表
|
||||
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
|
||||
// 支付方式弹窗
|
||||
const [payMethodModalVisible, setPayMethodModalVisible] = useState(false);
|
||||
// 当前编辑的索引,-1 表示新增
|
||||
const [editingPayMethodIndex, setEditingPayMethodIndex] = useState(-1);
|
||||
// 弹窗内表单字段的临时状态
|
||||
const [payMethodForm, setPayMethodForm] = useState({
|
||||
name: '',
|
||||
icon: '',
|
||||
payMethodType: '',
|
||||
payMethodName: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (props.options && formApiRef.current) {
|
||||
const currentInputs = {
|
||||
WaffoEnabled: props.options.WaffoEnabled === 'true' || props.options.WaffoEnabled === true,
|
||||
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',
|
||||
WaffoMerchantId: props.options.WaffoMerchantId || '',
|
||||
WaffoCurrency: props.options.WaffoCurrency || 'USD',
|
||||
WaffoUnitPrice: parseFloat(props.options.WaffoUnitPrice) || 1.0,
|
||||
WaffoMinTopUp: parseInt(props.options.WaffoMinTopUp) || 1,
|
||||
WaffoNotifyUrl: props.options.WaffoNotifyUrl || '',
|
||||
WaffoReturnUrl: props.options.WaffoReturnUrl || '',
|
||||
};
|
||||
setInputs(currentInputs);
|
||||
setOriginInputs({ ...currentInputs });
|
||||
formApiRef.current.setValues(currentInputs);
|
||||
|
||||
// 解析支付方式列表
|
||||
try {
|
||||
const rawPayMethods = props.options.WaffoPayMethods;
|
||||
if (rawPayMethods) {
|
||||
const parsed = JSON.parse(rawPayMethods);
|
||||
if (Array.isArray(parsed)) {
|
||||
setWaffoPayMethods(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setWaffoPayMethods([]);
|
||||
}
|
||||
}
|
||||
}, [props.options]);
|
||||
|
||||
const handleFormChange = (values) => {
|
||||
setInputs(values);
|
||||
};
|
||||
|
||||
const submitWaffoSetting = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const options = [];
|
||||
|
||||
options.push({
|
||||
key: 'WaffoEnabled',
|
||||
value: inputs.WaffoEnabled ? 'true' : 'false',
|
||||
});
|
||||
|
||||
if (inputs.WaffoApiKey && inputs.WaffoApiKey !== '') {
|
||||
options.push({ key: 'WaffoApiKey', value: inputs.WaffoApiKey });
|
||||
}
|
||||
|
||||
if (inputs.WaffoPrivateKey && inputs.WaffoPrivateKey !== '') {
|
||||
options.push({ key: 'WaffoPrivateKey', value: inputs.WaffoPrivateKey });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
if (inputs.WaffoSandboxPrivateKey && inputs.WaffoSandboxPrivateKey !== '') {
|
||||
options.push({ key: 'WaffoSandboxPrivateKey', value: inputs.WaffoSandboxPrivateKey });
|
||||
}
|
||||
|
||||
options.push({
|
||||
key: 'WaffoSandbox',
|
||||
value: inputs.WaffoSandbox ? 'true' : 'false',
|
||||
});
|
||||
|
||||
options.push({ key: 'WaffoMerchantId', value: inputs.WaffoMerchantId || '' });
|
||||
options.push({ key: 'WaffoCurrency', value: inputs.WaffoCurrency || '' });
|
||||
|
||||
options.push({
|
||||
key: 'WaffoUnitPrice',
|
||||
value: String(inputs.WaffoUnitPrice || 1.0),
|
||||
});
|
||||
|
||||
options.push({
|
||||
key: 'WaffoMinTopUp',
|
||||
value: String(inputs.WaffoMinTopUp || 1),
|
||||
});
|
||||
|
||||
options.push({ key: 'WaffoNotifyUrl', value: inputs.WaffoNotifyUrl || '' });
|
||||
options.push({ key: 'WaffoReturnUrl', value: inputs.WaffoReturnUrl || '' });
|
||||
|
||||
// 保存支付方式列表
|
||||
options.push({
|
||||
key: 'WaffoPayMethods',
|
||||
value: JSON.stringify(waffoPayMethods),
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
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 openAddPayMethodModal = () => {
|
||||
setEditingPayMethodIndex(-1);
|
||||
setPayMethodForm({ name: '', icon: '', payMethodType: '', payMethodName: '' });
|
||||
setPayMethodModalVisible(true);
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEditPayMethodModal = (record, index) => {
|
||||
setEditingPayMethodIndex(index);
|
||||
setPayMethodForm({
|
||||
name: record.name || '',
|
||||
icon: record.icon || '',
|
||||
payMethodType: record.payMethodType || '',
|
||||
payMethodName: record.payMethodName || '',
|
||||
});
|
||||
setPayMethodModalVisible(true);
|
||||
};
|
||||
|
||||
// 确认保存弹窗(新增或编辑)
|
||||
const handlePayMethodModalOk = () => {
|
||||
if (!payMethodForm.name || payMethodForm.name.trim() === '') {
|
||||
showError(t('支付方式名称不能为空'));
|
||||
return;
|
||||
}
|
||||
const newMethod = {
|
||||
name: payMethodForm.name.trim(),
|
||||
icon: payMethodForm.icon.trim(),
|
||||
payMethodType: payMethodForm.payMethodType.trim(),
|
||||
payMethodName: payMethodForm.payMethodName.trim(),
|
||||
};
|
||||
if (editingPayMethodIndex === -1) {
|
||||
// 新增
|
||||
setWaffoPayMethods([...waffoPayMethods, newMethod]);
|
||||
} else {
|
||||
// 编辑
|
||||
const updated = [...waffoPayMethods];
|
||||
updated[editingPayMethodIndex] = newMethod;
|
||||
setWaffoPayMethods(updated);
|
||||
}
|
||||
setPayMethodModalVisible(false);
|
||||
};
|
||||
|
||||
// 删除支付方式
|
||||
const handleDeletePayMethod = (index) => {
|
||||
const updated = waffoPayMethods.filter((_, i) => i !== index);
|
||||
setWaffoPayMethods(updated);
|
||||
};
|
||||
|
||||
// 支付方式表格列定义
|
||||
const payMethodColumns = [
|
||||
{
|
||||
title: t('显示名称'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: t('图标'),
|
||||
dataIndex: 'icon',
|
||||
render: (text) =>
|
||||
text ? (
|
||||
<img
|
||||
src={text}
|
||||
alt='icon'
|
||||
style={{ width: 24, height: 24, objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<Text type='tertiary'>—</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('支付方式类型'),
|
||||
dataIndex: 'payMethodType',
|
||||
render: (text) => text || <Text type='tertiary'>—</Text>,
|
||||
},
|
||||
{
|
||||
title: t('支付方式名称'),
|
||||
dataIndex: 'payMethodName',
|
||||
render: (text) => text || <Text type='tertiary'>—</Text>,
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record, index) => (
|
||||
<Space>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() => openEditPayMethodModal(record, index)}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='danger'
|
||||
onClick={() => handleDeletePayMethod(index)}
|
||||
>
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
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>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t(
|
||||
'请在 Waffo 后台获取 API 密钥、商户 ID 以及 RSA 密钥对,并配置回调地址。',
|
||||
)}
|
||||
/>
|
||||
|
||||
<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.Switch
|
||||
field='WaffoEnabled'
|
||||
label={t('启用 Waffo')}
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
field='WaffoSandbox'
|
||||
label={t('沙盒模式')}
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
extraText={t('启用后将使用 Waffo 沙盒环境')}
|
||||
/>
|
||||
</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}>
|
||||
<Form.Input
|
||||
field='WaffoMerchantId'
|
||||
label={t('商户 ID')}
|
||||
placeholder={t('Waffo 商户 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}>
|
||||
<Form.TextArea
|
||||
field='WaffoPrivateKey'
|
||||
label={t('RSA 私钥 (生产)')}
|
||||
placeholder={t('生产环境 RSA 私钥 Base64 (PKCS#8 DER)')}
|
||||
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}>
|
||||
<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)')}
|
||||
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={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='WaffoCurrency'
|
||||
label={t('货币')}
|
||||
disabled
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='WaffoUnitPrice'
|
||||
label={t('单价 (USD)')}
|
||||
placeholder='1.0'
|
||||
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'
|
||||
min={1}
|
||||
step={1}
|
||||
extraText={t('Waffo 充值的最低数量,默认 1')}
|
||||
/>
|
||||
</Col>
|
||||
<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')}
|
||||
/>
|
||||
</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')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<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('编辑支付方式')}
|
||||
visible={payMethodModalVisible}
|
||||
onOk={handlePayMethodModalOk}
|
||||
onCancel={() => setPayMethodModalVisible(false)}
|
||||
okText={t('确定')}
|
||||
cancelText={t('取消')}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text strong>{t('显示名称')}</Text>
|
||||
<span style={{ color: 'var(--semi-color-danger)', marginLeft: 4 }}>*</span>
|
||||
</div>
|
||||
<Input
|
||||
value={payMethodForm.name}
|
||||
onChange={(val) => setPayMethodForm({ ...payMethodForm, name: val })}
|
||||
placeholder={t('例如:Credit Card')}
|
||||
/>
|
||||
<Text type='tertiary' size='small'>{t('用户在充值页面看到的支付方式名称,例如:Credit Card')}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text strong>{t('图标')}</Text>
|
||||
</div>
|
||||
<Space align='center'>
|
||||
{payMethodForm.icon && (
|
||||
<img
|
||||
src={payMethodForm.icon}
|
||||
alt='preview'
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
objectFit: 'contain',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type='file'
|
||||
accept='image/*'
|
||||
ref={iconFileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleIconFileChange}
|
||||
/>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() => iconFileInputRef.current?.click()}
|
||||
>
|
||||
{payMethodForm.icon ? t('重新上传') : t('上传图片')}
|
||||
</Button>
|
||||
{payMethodForm.icon && (
|
||||
<Button
|
||||
size='small'
|
||||
type='danger'
|
||||
onClick={() =>
|
||||
setPayMethodForm((prev) => ({ ...prev, icon: '' }))
|
||||
}
|
||||
>
|
||||
{t('清除')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<Input
|
||||
value={payMethodForm.payMethodType}
|
||||
onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodType: val })}
|
||||
placeholder='CREDITCARD,DEBITCARD'
|
||||
maxLength={64}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<Input
|
||||
value={payMethodForm.payMethodName}
|
||||
onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodName: val })}
|
||||
placeholder={t('可空')}
|
||||
maxLength={64}
|
||||
/>
|
||||
<Text type='tertiary' size='small'>{t('Waffo API 参数,可空(最多64位)')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user