Commit 040e8c1d by CaIon

feat: replace quota input with amount-first UI and atomic quota adjustment

- Refactor token, redemption, and user quota inputs to prioritize monetary
  amount entry, with raw quota input collapsed by default
- Add atomic quota adjustment modal for users with add/subtract/override modes,
  bypassing batch update queue for immediate DB consistency
- Make user quota fields readonly in edit form; all modifications go through
  the dedicated adjust-quota modal via POST /api/user/manage
- Add DecreaseUserQuota `db` parameter for direct DB writes, matching
  IncreaseUserQuota behavior
- Support negative quota display in amount conversion helpers
- Add i18n keys for all new UI strings across all locales
parent 0664bb3f
...@@ -29,3 +29,5 @@ data/ ...@@ -29,3 +29,5 @@ data/
.gomodcache/ .gomodcache/
.gocache-temp .gocache-temp
.gopath .gopath
token_estimator_test.go
\ No newline at end of file
...@@ -572,9 +572,6 @@ func UpdateUser(c *gin.Context) { ...@@ -572,9 +572,6 @@ func UpdateUser(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
if originUser.Quota != updatedUser.Quota {
model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", logger.LogQuota(originUser.Quota), logger.LogQuota(updatedUser.Quota)))
}
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
...@@ -841,6 +838,8 @@ func CreateUser(c *gin.Context) { ...@@ -841,6 +838,8 @@ func CreateUser(c *gin.Context) {
type ManageRequest struct { type ManageRequest struct {
Id int `json:"id"` Id int `json:"id"`
Action string `json:"action"` Action string `json:"action"`
Value int `json:"value"`
Mode string `json:"mode"`
} }
// ManageUser Only admin user can do this // ManageUser Only admin user can do this
...@@ -907,6 +906,47 @@ func ManageUser(c *gin.Context) { ...@@ -907,6 +906,47 @@ func ManageUser(c *gin.Context) {
return return
} }
user.Role = common.RoleCommonUser user.Role = common.RoleCommonUser
case "add_quota":
switch req.Mode {
case "add":
if req.Value <= 0 {
common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero)
return
}
if err := model.IncreaseUserQuota(user.Id, req.Value, true); err != nil {
common.ApiError(c, err)
return
}
model.RecordLog(user.Id, model.LogTypeManage,
fmt.Sprintf("管理员增加用户额度 %s", logger.LogQuota(req.Value)))
case "subtract":
if req.Value <= 0 {
common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero)
return
}
if err := model.DecreaseUserQuota(user.Id, req.Value, true); err != nil {
common.ApiError(c, err)
return
}
model.RecordLog(user.Id, model.LogTypeManage,
fmt.Sprintf("管理员减少用户额度 %s", logger.LogQuota(req.Value)))
case "override":
oldQuota := user.Quota
if err := model.DB.Model(&model.User{}).Where("id = ?", user.Id).Update("quota", req.Value).Error; err != nil {
common.ApiError(c, err)
return
}
model.RecordLog(user.Id, model.LogTypeManage,
fmt.Sprintf("管理员覆盖用户额度从 %s 为 %s", logger.LogQuota(oldQuota), logger.LogQuota(req.Value)))
default:
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
} }
if err := user.Update(false); err != nil { if err := user.Update(false); err != nil {
......
...@@ -101,6 +101,7 @@ const ( ...@@ -101,6 +101,7 @@ const (
MsgUserTelegramIdEmpty = "user.telegram_id_empty" MsgUserTelegramIdEmpty = "user.telegram_id_empty"
MsgUserTelegramNotBound = "user.telegram_not_bound" MsgUserTelegramNotBound = "user.telegram_not_bound"
MsgUserLinuxDOIdEmpty = "user.linux_do_id_empty" MsgUserLinuxDOIdEmpty = "user.linux_do_id_empty"
MsgUserQuotaChangeZero = "user.quota_change_zero"
) )
// Quota related messages // Quota related messages
......
...@@ -91,6 +91,7 @@ user.wechat_id_empty: "WeChat ID is empty!" ...@@ -91,6 +91,7 @@ user.wechat_id_empty: "WeChat ID is empty!"
user.telegram_id_empty: "Telegram ID is empty!" user.telegram_id_empty: "Telegram ID is empty!"
user.telegram_not_bound: "This Telegram account is not bound" user.telegram_not_bound: "This Telegram account is not bound"
user.linux_do_id_empty: "Linux DO ID is empty!" user.linux_do_id_empty: "Linux DO ID is empty!"
user.quota_change_zero: "Quota change amount cannot be zero"
# Quota messages # Quota messages
quota.negative: "Quota cannot be negative!" quota.negative: "Quota cannot be negative!"
......
...@@ -92,6 +92,7 @@ user.wechat_id_empty: "WeChat id 为空!" ...@@ -92,6 +92,7 @@ user.wechat_id_empty: "WeChat id 为空!"
user.telegram_id_empty: "Telegram id 为空!" user.telegram_id_empty: "Telegram id 为空!"
user.telegram_not_bound: " Telegram 账户未绑定" user.telegram_not_bound: " Telegram 账户未绑定"
user.linux_do_id_empty: "Linux DO id 为空!" user.linux_do_id_empty: "Linux DO id 为空!"
user.quota_change_zero: "额度变更量不能为0"
# Quota messages # Quota messages
quota.negative: "额度不能为负数!" quota.negative: "额度不能为负数!"
......
...@@ -92,6 +92,7 @@ user.wechat_id_empty: "WeChat id 為空!" ...@@ -92,6 +92,7 @@ user.wechat_id_empty: "WeChat id 為空!"
user.telegram_id_empty: "Telegram id 為空!" user.telegram_id_empty: "Telegram id 為空!"
user.telegram_not_bound: " Telegram 帳號未綁定" user.telegram_not_bound: " Telegram 帳號未綁定"
user.linux_do_id_empty: "Linux DO id 為空!" user.linux_do_id_empty: "Linux DO id 為空!"
user.quota_change_zero: "額度變更量不能為0"
# Quota messages # Quota messages
quota.negative: "額度不能為負數!" quota.negative: "額度不能為負數!"
......
...@@ -523,7 +523,6 @@ func (user *User) Edit(updatePassword bool) error { ...@@ -523,7 +523,6 @@ func (user *User) Edit(updatePassword bool) error {
"username": newUser.Username, "username": newUser.Username,
"display_name": newUser.DisplayName, "display_name": newUser.DisplayName,
"group": newUser.Group, "group": newUser.Group,
"quota": newUser.Quota,
"remark": newUser.Remark, "remark": newUser.Remark,
} }
if updatePassword { if updatePassword {
...@@ -896,7 +895,7 @@ func increaseUserQuota(id int, quota int) (err error) { ...@@ -896,7 +895,7 @@ func increaseUserQuota(id int, quota int) (err error) {
return err return err
} }
func DecreaseUserQuota(id int, quota int) (err error) { func DecreaseUserQuota(id int, quota int, db bool) (err error) {
if quota < 0 { if quota < 0 {
return errors.New("quota 不能为负数!") return errors.New("quota 不能为负数!")
} }
...@@ -906,7 +905,7 @@ func DecreaseUserQuota(id int, quota int) (err error) { ...@@ -906,7 +905,7 @@ func DecreaseUserQuota(id int, quota int) (err error) {
common.SysLog("failed to decrease user quota: " + err.Error()) common.SysLog("failed to decrease user quota: " + err.Error())
} }
}) })
if common.BatchUpdateEnabled { if !db && common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeUserQuota, id, -quota) addNewRecord(BatchUpdateTypeUserQuota, id, -quota)
return nil return nil
} }
...@@ -928,7 +927,7 @@ func DeltaUpdateUserQuota(id int, delta int) (err error) { ...@@ -928,7 +927,7 @@ func DeltaUpdateUserQuota(id int, delta int) (err error) {
if delta > 0 { if delta > 0 {
return IncreaseUserQuota(id, delta, false) return IncreaseUserQuota(id, delta, false)
} else { } else {
return DecreaseUserQuota(id, -delta) return DecreaseUserQuota(id, -delta, false)
} }
} }
......
...@@ -37,7 +37,7 @@ func (w *WalletFunding) PreConsume(amount int) error { ...@@ -37,7 +37,7 @@ func (w *WalletFunding) PreConsume(amount int) error {
if amount <= 0 { if amount <= 0 {
return nil return nil
} }
if err := model.DecreaseUserQuota(w.userId, amount); err != nil { if err := model.DecreaseUserQuota(w.userId, amount, false); err != nil {
return err return err
} }
w.consumed = amount w.consumed = amount
...@@ -49,7 +49,7 @@ func (w *WalletFunding) Settle(delta int) error { ...@@ -49,7 +49,7 @@ func (w *WalletFunding) Settle(delta int) error {
return nil return nil
} }
if delta > 0 { if delta > 0 {
return model.DecreaseUserQuota(w.userId, delta) return model.DecreaseUserQuota(w.userId, delta, false)
} }
return model.IncreaseUserQuota(w.userId, -delta, false) return model.IncreaseUserQuota(w.userId, -delta, false)
} }
......
...@@ -381,7 +381,7 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu ...@@ -381,7 +381,7 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu
} else { } else {
// Wallet // Wallet
if quota > 0 { if quota > 0 {
err = model.DecreaseUserQuota(relayInfo.UserId, quota) err = model.DecreaseUserQuota(relayInfo.UserId, quota, false)
} else { } else {
err = model.IncreaseUserQuota(relayInfo.UserId, -quota, false) err = model.IncreaseUserQuota(relayInfo.UserId, -quota, false)
} }
......
...@@ -90,7 +90,7 @@ func taskAdjustFunding(task *model.Task, delta int) error { ...@@ -90,7 +90,7 @@ func taskAdjustFunding(task *model.Task, delta int) error {
return model.PostConsumeUserSubscriptionDelta(task.PrivateData.SubscriptionId, int64(delta)) return model.PostConsumeUserSubscriptionDelta(task.PrivateData.SubscriptionId, int64(delta))
} }
if delta > 0 { if delta > 0 {
return model.DecreaseUserQuota(task.UserId, delta) return model.DecreaseUserQuota(task.UserId, delta, false)
} }
return model.IncreaseUserQuota(task.UserId, -delta, false) return model.IncreaseUserQuota(task.UserId, -delta, false)
} }
......
...@@ -25,8 +25,12 @@ import { ...@@ -25,8 +25,12 @@ import {
showError, showError,
showSuccess, showSuccess,
renderQuota, renderQuota,
renderQuotaWithPrompt, getCurrencyConfig,
} from '../../../../helpers'; } from '../../../../helpers';
import {
quotaToDisplayAmount,
displayAmountToQuota,
} from '../../../../helpers/quota';
import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { useIsMobile } from '../../../../hooks/common/useIsMobile';
import { import {
Button, Button,
...@@ -41,6 +45,7 @@ import { ...@@ -41,6 +45,7 @@ import {
Avatar, Avatar,
Row, Row,
Col, Col,
InputNumber,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconCreditCard, IconCreditCard,
...@@ -57,10 +62,12 @@ const EditRedemptionModal = (props) => { ...@@ -57,10 +62,12 @@ const EditRedemptionModal = (props) => {
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const formApiRef = useRef(null); const formApiRef = useRef(null);
const [showQuotaInput, setShowQuotaInput] = useState(false);
const getInitValues = () => ({ const getInitValues = () => ({
name: '', name: '',
quota: 100000, quota: 100000,
amount: Number(quotaToDisplayAmount(100000).toFixed(6)),
count: 1, count: 1,
expired_time: null, expired_time: null,
}); });
...@@ -79,6 +86,7 @@ const EditRedemptionModal = (props) => { ...@@ -79,6 +86,7 @@ const EditRedemptionModal = (props) => {
} else { } else {
data.expired_time = new Date(data.expired_time * 1000); data.expired_time = new Date(data.expired_time * 1000);
} }
data.amount = Number(quotaToDisplayAmount(data.quota || 0).toFixed(6));
formApiRef.current?.setValues({ ...getInitValues(), ...data }); formApiRef.current?.setValues({ ...getInitValues(), ...data });
} else { } else {
showError(message); showError(message);
...@@ -104,7 +112,12 @@ const EditRedemptionModal = (props) => { ...@@ -104,7 +112,12 @@ const EditRedemptionModal = (props) => {
setLoading(true); setLoading(true);
let localInputs = { ...values }; let localInputs = { ...values };
localInputs.count = parseInt(localInputs.count) || 0; localInputs.count = parseInt(localInputs.count) || 0;
localInputs.quota = parseInt(localInputs.quota) || 0; localInputs.quota = displayAmountToQuota(localInputs.amount);
if (localInputs.quota <= 0) {
showError(t('请输入金额'));
setLoading(false);
return;
}
localInputs.name = name; localInputs.name = name;
if (!localInputs.expired_time) { if (!localInputs.expired_time) {
localInputs.expired_time = 0; localInputs.expired_time = 0;
...@@ -285,37 +298,63 @@ const EditRedemptionModal = (props) => { ...@@ -285,37 +298,63 @@ const EditRedemptionModal = (props) => {
</div> </div>
<Row gutter={12}> <Row gutter={12}>
<Col span={12}> <Col span={24}>
<Form.AutoComplete <Form.InputNumber
field='quota' field='amount'
label={t('额度')} label={t('金额')}
placeholder={t('请输入额度')} prefix={getCurrencyConfig().symbol}
placeholder={t('输入金额')}
precision={6}
min={0}
step={0.000001}
style={{ width: '100%' }} style={{ width: '100%' }}
type='number' onChange={(val) => {
rules={[ const amount = val === '' || val == null ? 0 : val;
{ required: true, message: t('请输入额度') }, formApiRef.current?.setValue('amount', amount);
{ formApiRef.current?.setValue(
validator: (rule, v) => { 'quota',
const num = parseInt(v, 10); displayAmountToQuota(amount),
return num > 0 );
? Promise.resolve() }}
: Promise.reject(t('额度必须大于0'));
},
},
]}
extraText={renderQuotaWithPrompt(
Number(values.quota) || 0,
)}
data={[
{ value: 500000, label: '1$' },
{ value: 5000000, label: '10$' },
{ value: 25000000, label: '50$' },
{ value: 50000000, label: '100$' },
{ value: 250000000, label: '500$' },
{ value: 500000000, label: '1000$' },
]}
showClear showClear
/> />
<div
className='text-xs cursor-pointer mt-1'
style={{ color: 'var(--semi-color-text-2)' }}
onClick={() => setShowQuotaInput((v) => !v)}
>
{showQuotaInput
? `▾ ${t('收起原生额度输入')}`
: `▸ ${t('使用原生额度输入')}`}
</div>
<div style={{ display: showQuotaInput ? 'block' : 'none' }} className='mt-2'>
<Form.InputNumber
field='quota'
label={t('额度')}
placeholder={t('输入额度')}
rules={[
{ required: true, message: t('请输入额度') },
{
validator: (rule, v) => {
const num = parseInt(v, 10);
return num > 0
? Promise.resolve()
: Promise.reject(t('额度必须大于0'));
},
},
]}
onChange={(val) => {
const quota = val === '' || val == null ? 0 : val;
formApiRef.current?.setValue('quota', quota);
formApiRef.current?.setValue(
'amount',
Number(quotaToDisplayAmount(quota).toFixed(6)),
);
}}
style={{ width: '100%' }}
showClear
/>
</div>
</Col> </Col>
{!isEdit && ( {!isEdit && (
<Col span={12}> <Col span={12}>
......
...@@ -24,10 +24,14 @@ import { ...@@ -24,10 +24,14 @@ import {
showSuccess, showSuccess,
timestamp2string, timestamp2string,
renderGroupOption, renderGroupOption,
renderQuotaWithPrompt, getCurrencyConfig,
getModelCategories, getModelCategories,
selectFilter, selectFilter,
} from '../../../../helpers'; } from '../../../../helpers';
import {
quotaToDisplayAmount,
displayAmountToQuota,
} from '../../../../helpers/quota';
import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { useIsMobile } from '../../../../hooks/common/useIsMobile';
import { import {
Button, Button,
...@@ -41,6 +45,7 @@ import { ...@@ -41,6 +45,7 @@ import {
Form, Form,
Col, Col,
Row, Row,
InputNumber,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconCreditCard, IconCreditCard,
...@@ -62,11 +67,13 @@ const EditTokenModal = (props) => { ...@@ -62,11 +67,13 @@ const EditTokenModal = (props) => {
const formApiRef = useRef(null); const formApiRef = useRef(null);
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [showQuotaInput, setShowQuotaInput] = useState(false);
const isEdit = props.editingToken.id !== undefined; const isEdit = props.editingToken.id !== undefined;
const getInitValues = () => ({ const getInitValues = () => ({
name: '', name: '',
remain_quota: 0, remain_quota: 0,
remain_amount: 0,
expired_time: -1, expired_time: -1,
unlimited_quota: true, unlimited_quota: true,
model_limits_enabled: false, model_limits_enabled: false,
...@@ -162,6 +169,9 @@ const EditTokenModal = (props) => { ...@@ -162,6 +169,9 @@ const EditTokenModal = (props) => {
} else { } else {
data.model_limits = []; data.model_limits = [];
} }
data.remain_amount = Number(
quotaToDisplayAmount(data.remain_quota || 0).toFixed(6),
);
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValues({ ...getInitValues(), ...data }); formApiRef.current.setValues({ ...getInitValues(), ...data });
} }
...@@ -209,7 +219,14 @@ const EditTokenModal = (props) => { ...@@ -209,7 +219,14 @@ const EditTokenModal = (props) => {
setLoading(true); setLoading(true);
if (isEdit) { if (isEdit) {
let { tokenCount: _tc, ...localInputs } = values; let { tokenCount: _tc, ...localInputs } = values;
localInputs.remain_quota = parseInt(localInputs.remain_quota); localInputs.remain_quota = localInputs.unlimited_quota
? 0
: displayAmountToQuota(localInputs.remain_amount);
if (!localInputs.unlimited_quota && localInputs.remain_quota <= 0) {
showError(t('请输入金额'));
setLoading(false);
return;
}
if (localInputs.expired_time !== -1) { if (localInputs.expired_time !== -1) {
let time = Date.parse(localInputs.expired_time); let time = Date.parse(localInputs.expired_time);
if (isNaN(time)) { if (isNaN(time)) {
...@@ -245,7 +262,14 @@ const EditTokenModal = (props) => { ...@@ -245,7 +262,14 @@ const EditTokenModal = (props) => {
} else { } else {
localInputs.name = baseName; localInputs.name = baseName;
} }
localInputs.remain_quota = parseInt(localInputs.remain_quota); localInputs.remain_quota = localInputs.unlimited_quota
? 0
: displayAmountToQuota(localInputs.remain_amount);
if (!localInputs.unlimited_quota && localInputs.remain_quota <= 0) {
showError(t('请输入金额'));
setLoading(false);
break;
}
if (localInputs.expired_time !== -1) { if (localInputs.expired_time !== -1) {
let time = Date.parse(localInputs.expired_time); let time = Date.parse(localInputs.expired_time);
...@@ -497,29 +521,64 @@ const EditTokenModal = (props) => { ...@@ -497,29 +521,64 @@ const EditTokenModal = (props) => {
</div> </div>
<Row gutter={12}> <Row gutter={12}>
<Col span={24}> <Col span={24}>
<Form.AutoComplete <Form.InputNumber
field='remain_quota' field='remain_amount'
label={t('额度')} label={t('金额')}
placeholder={t('请输入额度')} prefix={getCurrencyConfig().symbol}
type='number' placeholder={t('输入金额')}
precision={6}
disabled={values.unlimited_quota} disabled={values.unlimited_quota}
extraText={renderQuotaWithPrompt(values.remain_quota)} min={0}
rules={ step={0.000001}
values.unlimited_quota onChange={(val) => {
? [] const amount = val === '' || val == null ? 0 : val;
: [{ required: true, message: t('请输入额度') }] formApiRef.current?.setValue('remain_amount', amount);
} formApiRef.current?.setValue(
data={[ 'remain_quota',
{ value: 500000, label: '1$' }, displayAmountToQuota(amount),
{ value: 5000000, label: '10$' }, );
{ value: 25000000, label: '50$' }, }}
{ value: 50000000, label: '100$' }, style={{ width: '100%' }}
{ value: 250000000, label: '500$' }, showClear
{ value: 500000000, label: '1000$' },
]}
/> />
</Col> </Col>
<Col span={24}> <Col span={24}>
<div
className='text-xs cursor-pointer mt-1'
style={{ color: 'var(--semi-color-text-2)' }}
onClick={() => setShowQuotaInput((v) => !v)}
>
{showQuotaInput
? `▾ ${t('收起原生额度输入')}`
: `▸ ${t('使用原生额度输入')}`}
</div>
<div style={{ display: showQuotaInput ? 'block' : 'none' }} className='mt-2'>
<Form.InputNumber
field='remain_quota'
label={t('额度')}
placeholder={t('输入额度')}
disabled={values.unlimited_quota}
min={0}
step={500000}
rules={
values.unlimited_quota
? []
: [{ required: true, message: t('请输入额度') }]
}
onChange={(val) => {
const quota = val === '' || val == null ? 0 : val;
formApiRef.current?.setValue('remain_quota', quota);
formApiRef.current?.setValue(
'remain_amount',
Number(quotaToDisplayAmount(quota).toFixed(6)),
);
}}
style={{ width: '100%' }}
showClear
/>
</div>
</Col>
<Col span={24}>
<Form.Switch <Form.Switch
field='unlimited_quota' field='unlimited_quota'
label={t('无限额度')} label={t('无限额度')}
......
/*
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 { getCurrencyConfig } from './render'; import { getCurrencyConfig } from './render';
export const getQuotaPerUnit = () => { export const getQuotaPerUnit = () => {
...@@ -7,19 +25,23 @@ export const getQuotaPerUnit = () => { ...@@ -7,19 +25,23 @@ export const getQuotaPerUnit = () => {
export const quotaToDisplayAmount = (quota) => { export const quotaToDisplayAmount = (quota) => {
const q = Number(quota || 0); const q = Number(quota || 0);
if (!Number.isFinite(q) || q <= 0) return 0; if (!Number.isFinite(q) || q === 0) return 0;
const sign = Math.sign(q);
const abs = Math.abs(q);
const { type, rate } = getCurrencyConfig(); const { type, rate } = getCurrencyConfig();
if (type === 'TOKENS') return q; if (type === 'TOKENS') return q;
const usd = q / getQuotaPerUnit(); const usd = abs / getQuotaPerUnit();
if (type === 'USD') return usd; if (type === 'USD') return sign * usd;
return usd * (rate || 1); return sign * usd * (rate || 1);
}; };
export const displayAmountToQuota = (amount) => { export const displayAmountToQuota = (amount) => {
const val = Number(amount || 0); const val = Number(amount || 0);
if (!Number.isFinite(val) || val <= 0) return 0; if (!Number.isFinite(val) || val === 0) return 0;
const sign = Math.sign(val);
const abs = Math.abs(val);
const { type, rate } = getCurrencyConfig(); const { type, rate } = getCurrencyConfig();
if (type === 'TOKENS') return Math.round(val); if (type === 'TOKENS') return Math.round(val);
const usd = type === 'USD' ? val : val / (rate || 1); const usd = type === 'USD' ? abs : abs / (rate || 1);
return Math.round(usd * getQuotaPerUnit()); return sign * Math.round(usd * getQuotaPerUnit());
}; };
...@@ -825,6 +825,8 @@ ...@@ -825,6 +825,8 @@
"原密码": "Original Password", "原密码": "Original Password",
"原生格式": "Native format", "原生格式": "Native format",
"原生额度": "Raw quota", "原生额度": "Raw quota",
"使用原生额度输入": "Use raw quota input",
"收起原生额度输入": "Hide raw quota input",
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Deduplication completed: {{before}} keys before deduplication, {{after}} keys after deduplication", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Deduplication completed: {{before}} keys before deduplication, {{after}} keys after deduplication",
"参与官方同步": "Participate in official sync", "参与官方同步": "Participate in official sync",
"参数": "parameter", "参数": "parameter",
...@@ -2166,6 +2168,14 @@ ...@@ -2166,6 +2168,14 @@
"添加键值对": "Add key-value pair", "添加键值对": "Add key-value pair",
"添加问答": "Add FAQ", "添加问答": "Add FAQ",
"添加额度": "Add quota", "添加额度": "Add quota",
"减少": "Subtract",
"覆盖": "Override",
"调整额度": "Adjust Quota",
"调整额度成功": "Quota adjusted successfully",
"当前额度": "Current quota",
"变更": "Change",
"预计结果": "Estimated result",
"正数为增加,负数为减少": "Positive to add, negative to subtract",
"清理不活跃缓存": "Clean up inactive cache", "清理不活跃缓存": "Clean up inactive cache",
"清理失败": "Cleanup failed", "清理失败": "Cleanup failed",
"清理方式": "Cleanup Mode", "清理方式": "Cleanup Mode",
......
...@@ -821,6 +821,8 @@ ...@@ -821,6 +821,8 @@
"原密码": "Mot de passe original", "原密码": "Mot de passe original",
"原生格式": "Format natif", "原生格式": "Format natif",
"原生额度": "Quota brut", "原生额度": "Quota brut",
"使用原生额度输入": "Saisir le quota brut",
"收起原生额度输入": "Masquer la saisie du quota brut",
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Doublons supprimés : {{before}} clés avant, {{after}} clés après", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Doublons supprimés : {{before}} clés avant, {{after}} clés après",
"参与官方同步": "Participer à la synchronisation officielle", "参与官方同步": "Participer à la synchronisation officielle",
"参数": "paramètre", "参数": "paramètre",
...@@ -2144,6 +2146,14 @@ ...@@ -2144,6 +2146,14 @@
"添加键值对": "Ajouter une paire clé-valeur", "添加键值对": "Ajouter une paire clé-valeur",
"添加问答": "Ajouter une FAQ", "添加问答": "Ajouter une FAQ",
"添加额度": "Ajouter un quota", "添加额度": "Ajouter un quota",
"减少": "Soustraire",
"覆盖": "Remplacer",
"调整额度": "Ajuster le quota",
"调整额度成功": "Quota ajusté avec succès",
"当前额度": "Quota actuel",
"变更": "Modification",
"预计结果": "Résultat estimé",
"正数为增加,负数为减少": "Positif pour ajouter, négatif pour soustraire",
"清理不活跃缓存": "Nettoyer le cache inactif", "清理不活跃缓存": "Nettoyer le cache inactif",
"清理失败": "Échec du nettoyage", "清理失败": "Échec du nettoyage",
"清理方式": "Mode de nettoyage", "清理方式": "Mode de nettoyage",
......
...@@ -812,6 +812,8 @@ ...@@ -812,6 +812,8 @@
"原密码": "現在のパスワード", "原密码": "現在のパスワード",
"原生格式": "ネイティブ形式", "原生格式": "ネイティブ形式",
"原生额度": "生クォータ", "原生额度": "生クォータ",
"使用原生额度输入": "生クォータで入力",
"收起原生额度输入": "生クォータ入力を非表示",
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "重複排除完了:重複排除前 {{before}} 個のAPIキー、重複排除後 {{after}} 個のAPIキー", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "重複排除完了:重複排除前 {{before}} 個のAPIキー、重複排除後 {{after}} 個のAPIキー",
"参与官方同步": "公式との同期", "参与官方同步": "公式との同期",
"参数": "パラメータ", "参数": "パラメータ",
...@@ -2127,6 +2129,14 @@ ...@@ -2127,6 +2129,14 @@
"添加键值对": "キー/値ペア追加", "添加键值对": "キー/値ペア追加",
"添加问答": "FAQ追加", "添加问答": "FAQ追加",
"添加额度": "残高追加", "添加额度": "残高追加",
"减少": "減少",
"覆盖": "上書き",
"调整额度": "残高調整",
"调整额度成功": "残高の調整に成功しました",
"当前额度": "現在の残高",
"变更": "変更",
"预计结果": "予想結果",
"正数为增加,负数为减少": "正の数で追加、負の数で減少",
"清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ", "清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ",
"清理失败": "クリーンアップに失敗しました", "清理失败": "クリーンアップに失敗しました",
"清理方式": "クリーンアップモード", "清理方式": "クリーンアップモード",
......
...@@ -827,6 +827,8 @@ ...@@ -827,6 +827,8 @@
"原密码": "Старый пароль", "原密码": "Старый пароль",
"原生格式": "Нативный формат", "原生格式": "Нативный формат",
"原生额度": "Исходный лимит", "原生额度": "Исходный лимит",
"使用原生额度输入": "Ввод в исходных единицах",
"收起原生额度输入": "Скрыть ввод в исходных единицах",
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей",
"参与官方同步": "Участвовать в официальной синхронизации", "参与官方同步": "Участвовать в официальной синхронизации",
"参数": "Параметры", "参数": "Параметры",
...@@ -2156,6 +2158,14 @@ ...@@ -2156,6 +2158,14 @@
"添加键值对": "Добавить пару ключ-значение", "添加键值对": "Добавить пару ключ-значение",
"添加问答": "Добавить вопрос-ответ", "添加问答": "Добавить вопрос-ответ",
"添加额度": "Добавить лимит", "添加额度": "Добавить лимит",
"减少": "Уменьшить",
"覆盖": "Заменить",
"调整额度": "Скорректировать квоту",
"调整额度成功": "Квота успешно скорректирована",
"当前额度": "Текущая квота",
"变更": "Изменение",
"预计结果": "Ожидаемый результат",
"正数为增加,负数为减少": "Положительное для увеличения, отрицательное для уменьшения",
"清理不活跃缓存": "Очистить неактивный кэш", "清理不活跃缓存": "Очистить неактивный кэш",
"清理失败": "Ошибка очистки", "清理失败": "Ошибка очистки",
"清理方式": "Режим очистки", "清理方式": "Режим очистки",
......
...@@ -813,6 +813,8 @@ ...@@ -813,6 +813,8 @@
"原密码": "Mật khẩu cũ", "原密码": "Mật khẩu cũ",
"原生格式": "Định dạng gốc", "原生格式": "Định dạng gốc",
"原生额度": "Hạn mức gốc", "原生额度": "Hạn mức gốc",
"使用原生额度输入": "Nhập hạn mức gốc",
"收起原生额度输入": "Ẩn nhập hạn mức gốc",
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Hoàn tất loại bỏ trùng lặp: {{before}} khóa trước khi loại bỏ, {{after}} khóa sau khi loại bỏ", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Hoàn tất loại bỏ trùng lặp: {{before}} khóa trước khi loại bỏ, {{after}} khóa sau khi loại bỏ",
"参与官方同步": "Tham gia đồng bộ chính thức", "参与官方同步": "Tham gia đồng bộ chính thức",
"参数": "tham số", "参数": "tham số",
...@@ -2221,6 +2223,14 @@ ...@@ -2221,6 +2223,14 @@
"添加键值对": "Thêm cặp khóa-giá trị", "添加键值对": "Thêm cặp khóa-giá trị",
"添加问答": "Thêm hỏi đáp", "添加问答": "Thêm hỏi đáp",
"添加额度": "Thêm hạn ngạch", "添加额度": "Thêm hạn ngạch",
"减少": "Giảm",
"覆盖": "Ghi đè",
"调整额度": "Điều chỉnh hạn ngạch",
"调整额度成功": "Điều chỉnh hạn ngạch thành công",
"当前额度": "Hạn ngạch hiện tại",
"变更": "Thay đổi",
"预计结果": "Kết quả dự kiến",
"正数为增加,负数为减少": "Số dương để tăng, số âm để giảm",
"清理": "Dọn dẹp", "清理": "Dọn dẹp",
"清理不活跃缓存": "Xóa cache không hoạt động", "清理不活跃缓存": "Xóa cache không hoạt động",
"清理历史日志": "Dọn dẹp nhật ký lịch sử", "清理历史日志": "Dọn dẹp nhật ký lịch sử",
......
...@@ -1605,6 +1605,14 @@ ...@@ -1605,6 +1605,14 @@
"添加键值对": "添加键值对", "添加键值对": "添加键值对",
"添加问答": "添加问答", "添加问答": "添加问答",
"添加额度": "添加额度", "添加额度": "添加额度",
"减少": "减少",
"覆盖": "覆盖",
"调整额度": "调整额度",
"调整额度成功": "调整额度成功",
"当前额度": "当前额度",
"变更": "变更",
"预计结果": "预计结果",
"正数为增加,负数为减少": "正数为增加,负数为减少",
"清理方式": "清理方式", "清理方式": "清理方式",
"清理日志文件": "清理日志文件", "清理日志文件": "清理日志文件",
"清空": "清空", "清空": "清空",
...@@ -2737,6 +2745,8 @@ ...@@ -2737,6 +2745,8 @@
"请输入总额度": "请输入总额度", "请输入总额度": "请输入总额度",
"0 表示不限": "0 表示不限", "0 表示不限": "0 表示不限",
"原生额度": "原生额度", "原生额度": "原生额度",
"使用原生额度输入": "使用原生额度输入",
"收起原生额度输入": "收起原生额度输入",
"升级分组": "升级分组", "升级分组": "升级分组",
"不升级": "不升级", "不升级": "不升级",
"购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。": "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。", "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。": "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。",
......
...@@ -719,6 +719,8 @@ ...@@ -719,6 +719,8 @@
"原密码": "原密碼", "原密码": "原密碼",
"原生格式": "原生格式", "原生格式": "原生格式",
"原生额度": "原生額度", "原生额度": "原生額度",
"使用原生额度输入": "使用原生額度輸入",
"收起原生额度输入": "收起原生額度輸入",
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 個密鑰,去重後 {{after}} 個密鑰", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 個密鑰,去重後 {{after}} 個密鑰",
"参与官方同步": "參與官方同步", "参与官方同步": "參與官方同步",
"参数": "參數", "参数": "參數",
...@@ -1905,6 +1907,14 @@ ...@@ -1905,6 +1907,14 @@
"添加键值对": "添加鍵值對", "添加键值对": "添加鍵值對",
"添加问答": "添加問答", "添加问答": "添加問答",
"添加额度": "添加額度", "添加额度": "添加額度",
"减少": "減少",
"覆盖": "覆蓋",
"调整额度": "調整額度",
"调整额度成功": "調整額度成功",
"当前额度": "當前額度",
"变更": "變更",
"预计结果": "預計結果",
"正数为增加,负数为减少": "正數為增加,負數為減少",
"清理不活跃缓存": "清理不活躍快取", "清理不活跃缓存": "清理不活躍快取",
"清理失败": "清理失敗", "清理失败": "清理失敗",
"清理方式": "清理方式", "清理方式": "清理方式",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment