From 040e8c1da82af1b7cce819c9fe720877eca65556 Mon Sep 17 00:00:00 2001 From: CaIon Date: Thu, 9 Apr 2026 22:44:53 +0800 Subject: [PATCH] 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 --- .gitignore | 2 + controller/user.go | 46 ++- i18n/keys.go | 1 + i18n/locales/en.yaml | 1 + i18n/locales/zh-CN.yaml | 1 + i18n/locales/zh-TW.yaml | 1 + model/user.go | 7 +- service/funding_source.go | 4 +- service/quota.go | 2 +- service/task_billing.go | 2 +- .../modals/EditRedemptionModal.jsx | 99 +++++-- .../table/tokens/modals/EditTokenModal.jsx | 103 +++++-- .../table/users/modals/EditUserModal.jsx | 267 ++++++++++++------ web/src/helpers/quota.js | 36 ++- web/src/i18n/locales/en.json | 10 + web/src/i18n/locales/fr.json | 10 + web/src/i18n/locales/ja.json | 10 + web/src/i18n/locales/ru.json | 10 + web/src/i18n/locales/vi.json | 10 + web/src/i18n/locales/zh-CN.json | 10 + web/src/i18n/locales/zh-TW.json | 10 + 21 files changed, 493 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index 54fa8311..c17652a2 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ data/ .gomodcache/ .gocache-temp .gopath + +token_estimator_test.go \ No newline at end of file diff --git a/controller/user.go b/controller/user.go index 8229d0d2..8e26320f 100644 --- a/controller/user.go +++ b/controller/user.go @@ -572,9 +572,6 @@ func UpdateUser(c *gin.Context) { common.ApiError(c, err) 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{ "success": true, "message": "", @@ -841,6 +838,8 @@ func CreateUser(c *gin.Context) { type ManageRequest struct { Id int `json:"id"` Action string `json:"action"` + Value int `json:"value"` + Mode string `json:"mode"` } // ManageUser Only admin user can do this @@ -907,6 +906,47 @@ func ManageUser(c *gin.Context) { return } 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 { diff --git a/i18n/keys.go b/i18n/keys.go index 8118dff9..67ddbb9a 100644 --- a/i18n/keys.go +++ b/i18n/keys.go @@ -101,6 +101,7 @@ const ( MsgUserTelegramIdEmpty = "user.telegram_id_empty" MsgUserTelegramNotBound = "user.telegram_not_bound" MsgUserLinuxDOIdEmpty = "user.linux_do_id_empty" + MsgUserQuotaChangeZero = "user.quota_change_zero" ) // Quota related messages diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 75a8bc6e..3cb9ac2c 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -91,6 +91,7 @@ user.wechat_id_empty: "WeChat ID is empty!" user.telegram_id_empty: "Telegram ID is empty!" user.telegram_not_bound: "This Telegram account is not bound" user.linux_do_id_empty: "Linux DO ID is empty!" +user.quota_change_zero: "Quota change amount cannot be zero" # Quota messages quota.negative: "Quota cannot be negative!" diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index 1f3b5a7b..20f8a77b 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -92,6 +92,7 @@ user.wechat_id_empty: "WeChat id 为空!" user.telegram_id_empty: "Telegram id 为空!" user.telegram_not_bound: "该 Telegram 账户未绑定" user.linux_do_id_empty: "Linux DO id 为空!" +user.quota_change_zero: "额度变更量不能为0" # Quota messages quota.negative: "额度不能为负数!" diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml index 1231c0e2..887322c2 100644 --- a/i18n/locales/zh-TW.yaml +++ b/i18n/locales/zh-TW.yaml @@ -92,6 +92,7 @@ user.wechat_id_empty: "WeChat id 為空!" user.telegram_id_empty: "Telegram id 為空!" user.telegram_not_bound: "該 Telegram 帳號未綁定" user.linux_do_id_empty: "Linux DO id 為空!" +user.quota_change_zero: "額度變更量不能為0" # Quota messages quota.negative: "額度不能為負數!" diff --git a/model/user.go b/model/user.go index 1210b543..2bd1a8c3 100644 --- a/model/user.go +++ b/model/user.go @@ -523,7 +523,6 @@ func (user *User) Edit(updatePassword bool) error { "username": newUser.Username, "display_name": newUser.DisplayName, "group": newUser.Group, - "quota": newUser.Quota, "remark": newUser.Remark, } if updatePassword { @@ -896,7 +895,7 @@ func increaseUserQuota(id int, quota int) (err error) { return err } -func DecreaseUserQuota(id int, quota int) (err error) { +func DecreaseUserQuota(id int, quota int, db bool) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } @@ -906,7 +905,7 @@ func DecreaseUserQuota(id int, quota int) (err error) { common.SysLog("failed to decrease user quota: " + err.Error()) } }) - if common.BatchUpdateEnabled { + if !db && common.BatchUpdateEnabled { addNewRecord(BatchUpdateTypeUserQuota, id, -quota) return nil } @@ -928,7 +927,7 @@ func DeltaUpdateUserQuota(id int, delta int) (err error) { if delta > 0 { return IncreaseUserQuota(id, delta, false) } else { - return DecreaseUserQuota(id, -delta) + return DecreaseUserQuota(id, -delta, false) } } diff --git a/service/funding_source.go b/service/funding_source.go index 98f5e874..fc629c83 100644 --- a/service/funding_source.go +++ b/service/funding_source.go @@ -37,7 +37,7 @@ func (w *WalletFunding) PreConsume(amount int) error { if amount <= 0 { return nil } - if err := model.DecreaseUserQuota(w.userId, amount); err != nil { + if err := model.DecreaseUserQuota(w.userId, amount, false); err != nil { return err } w.consumed = amount @@ -49,7 +49,7 @@ func (w *WalletFunding) Settle(delta int) error { return nil } if delta > 0 { - return model.DecreaseUserQuota(w.userId, delta) + return model.DecreaseUserQuota(w.userId, delta, false) } return model.IncreaseUserQuota(w.userId, -delta, false) } diff --git a/service/quota.go b/service/quota.go index 9dc84ab4..4150c444 100644 --- a/service/quota.go +++ b/service/quota.go @@ -381,7 +381,7 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu } else { // Wallet if quota > 0 { - err = model.DecreaseUserQuota(relayInfo.UserId, quota) + err = model.DecreaseUserQuota(relayInfo.UserId, quota, false) } else { err = model.IncreaseUserQuota(relayInfo.UserId, -quota, false) } diff --git a/service/task_billing.go b/service/task_billing.go index e5c406dd..6cf7a965 100644 --- a/service/task_billing.go +++ b/service/task_billing.go @@ -90,7 +90,7 @@ func taskAdjustFunding(task *model.Task, delta int) error { return model.PostConsumeUserSubscriptionDelta(task.PrivateData.SubscriptionId, int64(delta)) } if delta > 0 { - return model.DecreaseUserQuota(task.UserId, delta) + return model.DecreaseUserQuota(task.UserId, delta, false) } return model.IncreaseUserQuota(task.UserId, -delta, false) } diff --git a/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx b/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx index bcde7260..e8352a63 100644 --- a/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx +++ b/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx @@ -25,8 +25,12 @@ import { showError, showSuccess, renderQuota, - renderQuotaWithPrompt, + getCurrencyConfig, } from '../../../../helpers'; +import { + quotaToDisplayAmount, + displayAmountToQuota, +} from '../../../../helpers/quota'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { Button, @@ -41,6 +45,7 @@ import { Avatar, Row, Col, + InputNumber, } from '@douyinfe/semi-ui'; import { IconCreditCard, @@ -57,10 +62,12 @@ const EditRedemptionModal = (props) => { const [loading, setLoading] = useState(isEdit); const isMobile = useIsMobile(); const formApiRef = useRef(null); + const [showQuotaInput, setShowQuotaInput] = useState(false); const getInitValues = () => ({ name: '', quota: 100000, + amount: Number(quotaToDisplayAmount(100000).toFixed(6)), count: 1, expired_time: null, }); @@ -79,6 +86,7 @@ const EditRedemptionModal = (props) => { } else { data.expired_time = new Date(data.expired_time * 1000); } + data.amount = Number(quotaToDisplayAmount(data.quota || 0).toFixed(6)); formApiRef.current?.setValues({ ...getInitValues(), ...data }); } else { showError(message); @@ -104,7 +112,12 @@ const EditRedemptionModal = (props) => { setLoading(true); let localInputs = { ...values }; 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; if (!localInputs.expired_time) { localInputs.expired_time = 0; @@ -285,37 +298,63 @@ const EditRedemptionModal = (props) => { - - + { - const num = parseInt(v, 10); - 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$' }, - ]} + onChange={(val) => { + const amount = val === '' || val == null ? 0 : val; + formApiRef.current?.setValue('amount', amount); + formApiRef.current?.setValue( + 'quota', + displayAmountToQuota(amount), + ); + }} showClear /> +
setShowQuotaInput((v) => !v)} + > + {showQuotaInput + ? `▾ ${t('收起原生额度输入')}` + : `▸ ${t('使用原生额度输入')}`} +
+
+ { + 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 + /> +
{!isEdit && ( diff --git a/web/src/components/table/tokens/modals/EditTokenModal.jsx b/web/src/components/table/tokens/modals/EditTokenModal.jsx index 93664580..9112658b 100644 --- a/web/src/components/table/tokens/modals/EditTokenModal.jsx +++ b/web/src/components/table/tokens/modals/EditTokenModal.jsx @@ -24,10 +24,14 @@ import { showSuccess, timestamp2string, renderGroupOption, - renderQuotaWithPrompt, + getCurrencyConfig, getModelCategories, selectFilter, } from '../../../../helpers'; +import { + quotaToDisplayAmount, + displayAmountToQuota, +} from '../../../../helpers/quota'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { Button, @@ -41,6 +45,7 @@ import { Form, Col, Row, + InputNumber, } from '@douyinfe/semi-ui'; import { IconCreditCard, @@ -62,11 +67,13 @@ const EditTokenModal = (props) => { const formApiRef = useRef(null); const [models, setModels] = useState([]); const [groups, setGroups] = useState([]); + const [showQuotaInput, setShowQuotaInput] = useState(false); const isEdit = props.editingToken.id !== undefined; const getInitValues = () => ({ name: '', remain_quota: 0, + remain_amount: 0, expired_time: -1, unlimited_quota: true, model_limits_enabled: false, @@ -162,6 +169,9 @@ const EditTokenModal = (props) => { } else { data.model_limits = []; } + data.remain_amount = Number( + quotaToDisplayAmount(data.remain_quota || 0).toFixed(6), + ); if (formApiRef.current) { formApiRef.current.setValues({ ...getInitValues(), ...data }); } @@ -209,7 +219,14 @@ const EditTokenModal = (props) => { setLoading(true); if (isEdit) { 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) { let time = Date.parse(localInputs.expired_time); if (isNaN(time)) { @@ -245,7 +262,14 @@ const EditTokenModal = (props) => { } else { 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) { let time = Date.parse(localInputs.expired_time); @@ -497,28 +521,63 @@ const EditTokenModal = (props) => { - { + const amount = val === '' || val == null ? 0 : val; + formApiRef.current?.setValue('remain_amount', amount); + formApiRef.current?.setValue( + 'remain_quota', + displayAmountToQuota(amount), + ); + }} + style={{ width: '100%' }} + showClear /> + +
setShowQuotaInput((v) => !v)} + > + {showQuotaInput + ? `▾ ${t('收起原生额度输入')}` + : `▸ ${t('使用原生额度输入')}`} +
+
+ { + 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 + /> +
+ { const { t } = useTranslation(); const userId = props.editingUser.id; const [loading, setLoading] = useState(true); - const [addQuotaModalOpen, setIsModalOpen] = useState(false); - const [addQuotaLocal, setAddQuotaLocal] = useState(''); - const [addAmountLocal, setAddAmountLocal] = useState(''); + const [adjustModalOpen, setAdjustModalOpen] = useState(false); + const [adjustQuotaLocal, setAdjustQuotaLocal] = useState(''); + const [adjustAmountLocal, setAdjustAmountLocal] = useState(''); + const [adjustMode, setAdjustMode] = useState('add'); + const [adjustLoading, setAdjustLoading] = useState(false); const isMobile = useIsMobile(); const [groupOptions, setGroupOptions] = useState([]); const [bindingModalVisible, setBindingModalVisible] = useState(false); const formApiRef = useRef(null); + const [showAdjustQuotaRaw, setShowAdjustQuotaRaw] = useState(false); + const [showQuotaInput, setShowQuotaInput] = useState(false); + const [inputs, setInputs] = useState(null); const isEdit = Boolean(userId); @@ -85,6 +91,7 @@ const EditUserModal = (props) => { linux_do_id: '', email: '', quota: 0, + quota_amount: 0, group: 'default', remark: '', }); @@ -107,13 +114,22 @@ const EditUserModal = (props) => { const { success, message, data } = res.data; if (success) { data.password = ''; - formApiRef.current?.setValues({ ...getInitValues(), ...data }); + data.quota_amount = Number( + quotaToDisplayAmount(data.quota || 0).toFixed(6), + ); + setInputs({ ...getInitValues(), ...data }); } else { showError(message); } setLoading(false); }; + useEffect(() => { + if (inputs && formApiRef.current) { + formApiRef.current.setValues(inputs); + } + }, [inputs]); + useEffect(() => { loadUser(); if (userId) fetchGroups(); @@ -132,8 +148,8 @@ const EditUserModal = (props) => { const submit = async (values) => { setLoading(true); let payload = { ...values }; - if (typeof payload.quota === 'string') - payload.quota = parseInt(payload.quota) || 0; + delete payload.quota; + delete payload.quota_amount; if (userId) { payload.id = parseInt(userId); } @@ -150,11 +166,60 @@ const EditUserModal = (props) => { setLoading(false); }; - /* --------------------- quota helper -------------------- */ - const addLocalQuota = () => { - const current = parseInt(formApiRef.current?.getValue('quota') || 0); - const delta = parseInt(addQuotaLocal) || 0; - formApiRef.current?.setValue('quota', current + delta); + /* --------------------- atomic quota adjust -------------------- */ + const adjustQuota = async () => { + const quotaVal = parseInt(adjustQuotaLocal) || 0; + if (quotaVal <= 0 && adjustMode !== 'override') return; + if (adjustMode === 'override' && (adjustQuotaLocal === '' || adjustQuotaLocal == null)) return; + setAdjustLoading(true); + try { + const res = await API.post('/api/user/manage', { + id: parseInt(userId), + action: 'add_quota', + mode: adjustMode, + value: adjustMode === 'override' ? quotaVal : Math.abs(quotaVal), + }); + const { success, message } = res.data; + if (success) { + showSuccess(t('调整额度成功')); + setAdjustModalOpen(false); + setAdjustQuotaLocal(''); + setAdjustAmountLocal(''); + const userRes = await API.get(`/api/user/${userId}`); + if (userRes.data.success) { + const data = userRes.data.data; + data.password = ''; + data.quota_amount = Number( + quotaToDisplayAmount(data.quota || 0).toFixed(6), + ); + setInputs({ ...getInitValues(), ...data }); + } + props.refresh(); + } else { + showError(message); + } + } catch (e) { + showError(e.message); + } + setAdjustLoading(false); + }; + + const getPreviewText = () => { + const current = formApiRef.current?.getValue('quota') || 0; + const val = parseInt(adjustQuotaLocal) || 0; + let result; + switch (adjustMode) { + case 'add': + result = current + Math.abs(val); + return `${t('当前额度')}:${renderQuota(current)},+${renderQuota(Math.abs(val))} = ${renderQuota(result)}`; + case 'subtract': + result = current - Math.abs(val); + return `${t('当前额度')}:${renderQuota(current)},-${renderQuota(Math.abs(val))} = ${renderQuota(result)}`; + case 'override': + return `${t('当前额度')}:${renderQuota(current)} → ${renderQuota(val)}`; + default: + return ''; + } }; /* --------------------------- UI --------------------------- */ @@ -305,24 +370,47 @@ const EditUserModal = (props) => { - + + + +
setShowQuotaInput((v) => !v)} + > + {showQuotaInput + ? `▾ ${t('收起原生额度输入')}` + : `▸ ${t('使用原生额度输入')}`} +
+
+ +
+
)} @@ -372,81 +460,102 @@ const EditUserModal = (props) => { formApiRef={formApiRef} /> - {/* 添加额度模态框 */} + {/* 调整额度模态框 */} { - addLocalQuota(); - setIsModalOpen(false); - setAddQuotaLocal(''); - setAddAmountLocal(''); - }} + visible={adjustModalOpen} + onOk={adjustQuota} onCancel={() => { - setIsModalOpen(false); + setAdjustModalOpen(false); + setAdjustQuotaLocal(''); + setAdjustAmountLocal(''); + setAdjustMode('add'); }} + confirmLoading={adjustLoading} closable={null} title={
- - {t('添加额度')} + + {t('调整额度')}
} >
- {(() => { - const current = formApiRef.current?.getValue('quota') || 0; - return ( - - {`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`} - - ); - })()} + + {getPreviewText()} +
- {getCurrencyConfig().type !== 'TOKENS' && ( -
-
- {t('金额')} - - {' '} - ({t('仅用于换算,实际保存的是额度')}) - -
- { - setAddAmountLocal(val); - setAddQuotaLocal( - val != null && val !== '' - ? displayAmountToQuota(Math.abs(val)) * Math.sign(val) - : '', - ); - }} - style={{ width: '100%' }} - showClear - /> +
+
+ {t('操作')}
- )} -
+ { + setAdjustMode(e.target.value); + setAdjustQuotaLocal(''); + setAdjustAmountLocal(''); + }} + style={{ width: '100%' }} + > + {t('添加')} + {t('减少')} + {t('覆盖')} + +
+
+
+ {t('金额')} +
+ { + const amount = val === '' || val == null ? '' : val; + setAdjustAmountLocal(amount); + setAdjustQuotaLocal( + amount === '' + ? '' + : adjustMode === 'override' + ? displayAmountToQuota(amount) + : displayAmountToQuota(Math.abs(amount)), + ); + }} + style={{ width: '100%' }} + showClear + /> +
+
setShowAdjustQuotaRaw((v) => !v)} + > + {showAdjustQuotaRaw + ? `▾ ${t('收起原生额度输入')}` + : `▸ ${t('使用原生额度输入')}`} +
+
{t('额度')}
{ - setAddQuotaLocal(val); - setAddAmountLocal( - val != null && val !== '' - ? Number( - ( - quotaToDisplayAmount(Math.abs(val)) * Math.sign(val) - ).toFixed(2), - ) - : '', + const quota = val === '' || val == null ? '' : val; + setAdjustQuotaLocal(quota); + setAdjustAmountLocal( + quota === '' + ? '' + : adjustMode === 'override' + ? Number(quotaToDisplayAmount(quota).toFixed(6)) + : Number(quotaToDisplayAmount(Math.abs(quota)).toFixed(6)), ); }} style={{ width: '100%' }} diff --git a/web/src/helpers/quota.js b/web/src/helpers/quota.js index 2733af24..1d08aa68 100644 --- a/web/src/helpers/quota.js +++ b/web/src/helpers/quota.js @@ -1,3 +1,21 @@ +/* +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 { getCurrencyConfig } from './render'; export const getQuotaPerUnit = () => { @@ -7,19 +25,23 @@ export const getQuotaPerUnit = () => { export const quotaToDisplayAmount = (quota) => { 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(); if (type === 'TOKENS') return q; - const usd = q / getQuotaPerUnit(); - if (type === 'USD') return usd; - return usd * (rate || 1); + const usd = abs / getQuotaPerUnit(); + if (type === 'USD') return sign * usd; + return sign * usd * (rate || 1); }; export const displayAmountToQuota = (amount) => { 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(); if (type === 'TOKENS') return Math.round(val); - const usd = type === 'USD' ? val : val / (rate || 1); - return Math.round(usd * getQuotaPerUnit()); + const usd = type === 'USD' ? abs : abs / (rate || 1); + return sign * Math.round(usd * getQuotaPerUnit()); }; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 52ef677f..1a2841c1 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -825,6 +825,8 @@ "原密码": "Original Password", "原生格式": "Native format", "原生额度": "Raw quota", + "使用原生额度输入": "Use raw quota input", + "收起原生额度输入": "Hide raw quota input", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Deduplication completed: {{before}} keys before deduplication, {{after}} keys after deduplication", "参与官方同步": "Participate in official sync", "参数": "parameter", @@ -2166,6 +2168,14 @@ "添加键值对": "Add key-value pair", "添加问答": "Add FAQ", "添加额度": "Add quota", + "减少": "Subtract", + "覆盖": "Override", + "调整额度": "Adjust Quota", + "调整额度成功": "Quota adjusted successfully", + "当前额度": "Current quota", + "变更": "Change", + "预计结果": "Estimated result", + "正数为增加,负数为减少": "Positive to add, negative to subtract", "清理不活跃缓存": "Clean up inactive cache", "清理失败": "Cleanup failed", "清理方式": "Cleanup Mode", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index bbec562f..db603ed3 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -821,6 +821,8 @@ "原密码": "Mot de passe original", "原生格式": "Format natif", "原生额度": "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", "参与官方同步": "Participer à la synchronisation officielle", "参数": "paramètre", @@ -2144,6 +2146,14 @@ "添加键值对": "Ajouter une paire clé-valeur", "添加问答": "Ajouter une FAQ", "添加额度": "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", "清理失败": "Échec du nettoyage", "清理方式": "Mode de nettoyage", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 11ac9365..c449048a 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -812,6 +812,8 @@ "原密码": "現在のパスワード", "原生格式": "ネイティブ形式", "原生额度": "生クォータ", + "使用原生额度输入": "生クォータで入力", + "收起原生额度输入": "生クォータ入力を非表示", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "重複排除完了:重複排除前 {{before}} 個のAPIキー、重複排除後 {{after}} 個のAPIキー", "参与官方同步": "公式との同期", "参数": "パラメータ", @@ -2127,6 +2129,14 @@ "添加键值对": "キー/値ペア追加", "添加问答": "FAQ追加", "添加额度": "残高追加", + "减少": "減少", + "覆盖": "上書き", + "调整额度": "残高調整", + "调整额度成功": "残高の調整に成功しました", + "当前额度": "現在の残高", + "变更": "変更", + "预计结果": "予想結果", + "正数为增加,负数为减少": "正の数で追加、負の数で減少", "清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ", "清理失败": "クリーンアップに失敗しました", "清理方式": "クリーンアップモード", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index aa7127b7..54f79adf 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -827,6 +827,8 @@ "原密码": "Старый пароль", "原生格式": "Нативный формат", "原生额度": "Исходный лимит", + "使用原生额度输入": "Ввод в исходных единицах", + "收起原生额度输入": "Скрыть ввод в исходных единицах", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей", "参与官方同步": "Участвовать в официальной синхронизации", "参数": "Параметры", @@ -2156,6 +2158,14 @@ "添加键值对": "Добавить пару ключ-значение", "添加问答": "Добавить вопрос-ответ", "添加额度": "Добавить лимит", + "减少": "Уменьшить", + "覆盖": "Заменить", + "调整额度": "Скорректировать квоту", + "调整额度成功": "Квота успешно скорректирована", + "当前额度": "Текущая квота", + "变更": "Изменение", + "预计结果": "Ожидаемый результат", + "正数为增加,负数为减少": "Положительное для увеличения, отрицательное для уменьшения", "清理不活跃缓存": "Очистить неактивный кэш", "清理失败": "Ошибка очистки", "清理方式": "Режим очистки", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 7497cf7f..8ec57416 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -813,6 +813,8 @@ "原密码": "Mật khẩu cũ", "原生格式": "Định dạng 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ỏ", "参与官方同步": "Tham gia đồng bộ chính thức", "参数": "tham số", @@ -2221,6 +2223,14 @@ "添加键值对": "Thêm cặp khóa-giá trị", "添加问答": "Thêm hỏi đáp", "添加额度": "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", "清理不活跃缓存": "Xóa cache không hoạt động", "清理历史日志": "Dọn dẹp nhật ký lịch sử", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 69e824e6..c1142a08 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -1605,6 +1605,14 @@ "添加键值对": "添加键值对", "添加问答": "添加问答", "添加额度": "添加额度", + "减少": "减少", + "覆盖": "覆盖", + "调整额度": "调整额度", + "调整额度成功": "调整额度成功", + "当前额度": "当前额度", + "变更": "变更", + "预计结果": "预计结果", + "正数为增加,负数为减少": "正数为增加,负数为减少", "清理方式": "清理方式", "清理日志文件": "清理日志文件", "清空": "清空", @@ -2737,6 +2745,8 @@ "请输入总额度": "请输入总额度", "0 表示不限": "0 表示不限", "原生额度": "原生额度", + "使用原生额度输入": "使用原生额度输入", + "收起原生额度输入": "收起原生额度输入", "升级分组": "升级分组", "不升级": "不升级", "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。": "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index ac853e4b..a5cea876 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -719,6 +719,8 @@ "原密码": "原密碼", "原生格式": "原生格式", "原生额度": "原生額度", + "使用原生额度输入": "使用原生額度輸入", + "收起原生额度输入": "收起原生額度輸入", "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 個密鑰,去重後 {{after}} 個密鑰", "参与官方同步": "參與官方同步", "参数": "參數", @@ -1905,6 +1907,14 @@ "添加键值对": "添加鍵值對", "添加问答": "添加問答", "添加额度": "添加額度", + "减少": "減少", + "覆盖": "覆蓋", + "调整额度": "調整額度", + "调整额度成功": "調整額度成功", + "当前额度": "當前額度", + "变更": "變更", + "预计结果": "預計結果", + "正数为增加,负数为减少": "正數為增加,負數為減少", "清理不活跃缓存": "清理不活躍快取", "清理失败": "清理失敗", "清理方式": "清理方式",