/*
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 .
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef, useMemo } from 'react';
import {
Banner,
Button,
Col,
Form,
Row,
Spin,
Modal,
Input,
Typography,
} from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
export default function GeneralSettings(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [showQuotaWarning, setShowQuotaWarning] = useState(false);
const [inputs, setInputs] = useState({
TopUpLink: '',
'general_setting.docs_link': '',
'general_setting.quota_display_type': 'USD',
'general_setting.custom_currency_symbol': '¤',
'general_setting.custom_currency_exchange_rate': '',
QuotaPerUnit: '',
RetryTimes: '',
USDExchangeRate: '',
DisplayTokenStatEnabled: false,
DefaultCollapseSidebar: false,
DemoSiteEnabled: false,
SelfUseModeEnabled: false,
'token_setting.max_user_tokens': 1000,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function handleFieldChange(fieldName) {
return (value) => {
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
};
}
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}
// 计算展示在输入框中的“1 USD = X ”中的 X
const combinedRate = useMemo(() => {
const type = inputs['general_setting.quota_display_type'];
if (type === 'USD') return '1';
if (type === 'CNY') return String(inputs['USDExchangeRate'] || '');
if (type === 'TOKENS') return String(inputs['QuotaPerUnit'] || '');
if (type === 'CUSTOM')
return String(
inputs['general_setting.custom_currency_exchange_rate'] || '',
);
return '';
}, [inputs]);
const onCombinedRateChange = (val) => {
const type = inputs['general_setting.quota_display_type'];
if (type === 'CNY') {
handleFieldChange('USDExchangeRate')(val);
} else if (type === 'TOKENS') {
handleFieldChange('QuotaPerUnit')(val);
} else if (type === 'CUSTOM') {
handleFieldChange('general_setting.custom_currency_exchange_rate')(val);
}
};
const showTokensOption = useMemo(() => {
const initialType = props.options?.['general_setting.quota_display_type'];
const initialQuotaPerUnit = parseFloat(props.options?.QuotaPerUnit);
const legacyTokensMode =
initialType === undefined &&
props.options?.DisplayInCurrencyEnabled !== undefined &&
!props.options.DisplayInCurrencyEnabled;
return (
initialType === 'TOKENS' ||
legacyTokensMode ||
(!isNaN(initialQuotaPerUnit) && initialQuotaPerUnit !== 500000)
);
}, [props.options]);
const quotaDisplayType = inputs['general_setting.quota_display_type'];
const quotaDisplayTypeDesc = useMemo(() => {
const descMap = {
USD: t('站点所有额度将以美元 ($) 显示'),
CNY: t('站点所有额度将按汇率换算为人民币 (¥) 显示'),
TOKENS: t('站点所有额度将以原始 Token 数显示,不做货币换算'),
CUSTOM: t('站点所有额度将按汇率换算为自定义货币显示'),
};
return descMap[quotaDisplayType] || '';
}, [quotaDisplayType, t]);
const rateLabel = useMemo(() => {
if (quotaDisplayType === 'CNY') return t('汇率');
if (quotaDisplayType === 'TOKENS') return t('每美元对应 Token 数');
if (quotaDisplayType === 'CUSTOM') return t('汇率');
return '';
}, [quotaDisplayType, t]);
const rateSuffix = useMemo(() => {
if (quotaDisplayType === 'CNY') return 'CNY (¥)';
if (quotaDisplayType === 'TOKENS') return 'Tokens';
if (quotaDisplayType === 'CUSTOM')
return inputs['general_setting.custom_currency_symbol'] || '¤';
return '';
}, [quotaDisplayType, inputs]);
const rateExtraText = useMemo(() => {
if (quotaDisplayType === 'CNY')
return t(
'系统内部以美元 (USD) 为基准计价。用户余额、充值金额、模型定价、用量日志等所有金额显示均按此汇率换算为人民币,不影响内部计费',
);
if (quotaDisplayType === 'TOKENS')
return t(
'系统内部计费精度,默认 500000,修改可能导致计费异常,请谨慎操作',
);
if (quotaDisplayType === 'CUSTOM')
return t(
'系统内部以美元 (USD) 为基准计价。用户余额、充值金额、模型定价、用量日志等所有金额显示均按此汇率换算为自定义货币,不影响内部计费',
);
return '';
}, [quotaDisplayType, t]);
const previewText = useMemo(() => {
if (quotaDisplayType === 'USD') return '$1.00';
const rate = parseFloat(combinedRate);
if (!rate || isNaN(rate)) return t('请输入汇率');
if (quotaDisplayType === 'CNY') return `$1.00 → ¥${rate.toFixed(2)}`;
if (quotaDisplayType === 'TOKENS')
return `$1.00 → ${Number(rate).toLocaleString()} Tokens`;
if (quotaDisplayType === 'CUSTOM') {
const symbol = inputs['general_setting.custom_currency_symbol'] || '¤';
return `$1.00 → ${symbol}${rate.toFixed(2)}`;
}
return '';
}, [quotaDisplayType, combinedRate, inputs, t]);
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
// 若旧字段存在且新字段缺失,则做一次兜底映射
if (
currentInputs['general_setting.quota_display_type'] === undefined &&
props.options?.DisplayInCurrencyEnabled !== undefined
) {
currentInputs['general_setting.quota_display_type'] = props.options
.DisplayInCurrencyEnabled
? 'USD'
: 'TOKENS';
}
// 回填自定义货币相关字段(如果后端已存在)
if (props.options['general_setting.custom_currency_symbol'] !== undefined) {
currentInputs['general_setting.custom_currency_symbol'] =
props.options['general_setting.custom_currency_symbol'];
}
if (
props.options['general_setting.custom_currency_exchange_rate'] !==
undefined
) {
currentInputs['general_setting.custom_currency_exchange_rate'] =
props.options['general_setting.custom_currency_exchange_rate'];
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
{/* 单位美元额度已合入汇率组合控件(TOKENS 模式下编辑),不再单独展示 */}
USD ($)
CNY (¥)
{showTokensOption && (
Tokens
)}
{t('自定义货币')}
{quotaDisplayType !== 'USD' && (
{rateExtraText}
)}
{t('预览效果')}:{previewText}
setShowQuotaWarning(false)}
onCancel={() => setShowQuotaWarning(false)}
closeOnEsc={true}
width={500}
>
>
);
}