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
This commit is contained in:
@@ -29,3 +29,5 @@ data/
|
|||||||
.gomodcache/
|
.gomodcache/
|
||||||
.gocache-temp
|
.gocache-temp
|
||||||
.gopath
|
.gopath
|
||||||
|
|
||||||
|
token_estimator_test.go
|
||||||
+43
-3
@@ -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) {
|
|||||||
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) {
|
|||||||
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 (
|
|||||||
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!"
|
|||||||
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 为空!"
|
|||||||
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 為空!"
|
|||||||
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: "額度不能為負數!"
|
||||||
|
|||||||
+3
-4
@@ -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) {
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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) => {
|
|||||||
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) => {
|
|||||||
} 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) => {
|
|||||||
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) => {
|
|||||||
</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 {
|
|||||||
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 {
|
|||||||
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) => {
|
|||||||
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) => {
|
|||||||
} 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) => {
|
|||||||
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) => {
|
|||||||
} 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,28 +521,63 @@ 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}>
|
||||||
|
<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}>
|
<Col span={24}>
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field='unlimited_quota'
|
field='unlimited_quota'
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
showError,
|
showError,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
renderQuota,
|
renderQuota,
|
||||||
renderQuotaWithPrompt,
|
|
||||||
getCurrencyConfig,
|
getCurrencyConfig,
|
||||||
} from '../../../../helpers';
|
} from '../../../../helpers';
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +45,8 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconUser,
|
IconUser,
|
||||||
@@ -53,7 +54,7 @@ import {
|
|||||||
IconClose,
|
IconClose,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconUserGroup,
|
IconUserGroup,
|
||||||
IconPlus,
|
IconEdit,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import UserBindingManagementModal from './UserBindingManagementModal';
|
import UserBindingManagementModal from './UserBindingManagementModal';
|
||||||
|
|
||||||
@@ -63,13 +64,18 @@ const EditUserModal = (props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const userId = props.editingUser.id;
|
const userId = props.editingUser.id;
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
const [adjustModalOpen, setAdjustModalOpen] = useState(false);
|
||||||
const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
const [adjustQuotaLocal, setAdjustQuotaLocal] = useState('');
|
||||||
const [addAmountLocal, setAddAmountLocal] = useState('');
|
const [adjustAmountLocal, setAdjustAmountLocal] = useState('');
|
||||||
|
const [adjustMode, setAdjustMode] = useState('add');
|
||||||
|
const [adjustLoading, setAdjustLoading] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [groupOptions, setGroupOptions] = useState([]);
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
const [bindingModalVisible, setBindingModalVisible] = useState(false);
|
const [bindingModalVisible, setBindingModalVisible] = useState(false);
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
|
const [showAdjustQuotaRaw, setShowAdjustQuotaRaw] = useState(false);
|
||||||
|
const [showQuotaInput, setShowQuotaInput] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState(null);
|
||||||
|
|
||||||
const isEdit = Boolean(userId);
|
const isEdit = Boolean(userId);
|
||||||
|
|
||||||
@@ -85,6 +91,7 @@ const EditUserModal = (props) => {
|
|||||||
linux_do_id: '',
|
linux_do_id: '',
|
||||||
email: '',
|
email: '',
|
||||||
quota: 0,
|
quota: 0,
|
||||||
|
quota_amount: 0,
|
||||||
group: 'default',
|
group: 'default',
|
||||||
remark: '',
|
remark: '',
|
||||||
});
|
});
|
||||||
@@ -107,13 +114,22 @@ const EditUserModal = (props) => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
data.password = '';
|
data.password = '';
|
||||||
formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
data.quota_amount = Number(
|
||||||
|
quotaToDisplayAmount(data.quota || 0).toFixed(6),
|
||||||
|
);
|
||||||
|
setInputs({ ...getInitValues(), ...data });
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputs && formApiRef.current) {
|
||||||
|
formApiRef.current.setValues(inputs);
|
||||||
|
}
|
||||||
|
}, [inputs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUser();
|
loadUser();
|
||||||
if (userId) fetchGroups();
|
if (userId) fetchGroups();
|
||||||
@@ -132,8 +148,8 @@ const EditUserModal = (props) => {
|
|||||||
const submit = async (values) => {
|
const submit = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let payload = { ...values };
|
let payload = { ...values };
|
||||||
if (typeof payload.quota === 'string')
|
delete payload.quota;
|
||||||
payload.quota = parseInt(payload.quota) || 0;
|
delete payload.quota_amount;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
payload.id = parseInt(userId);
|
payload.id = parseInt(userId);
|
||||||
}
|
}
|
||||||
@@ -150,11 +166,60 @@ const EditUserModal = (props) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* --------------------- quota helper -------------------- */
|
/* --------------------- atomic quota adjust -------------------- */
|
||||||
const addLocalQuota = () => {
|
const adjustQuota = async () => {
|
||||||
const current = parseInt(formApiRef.current?.getValue('quota') || 0);
|
const quotaVal = parseInt(adjustQuotaLocal) || 0;
|
||||||
const delta = parseInt(addQuotaLocal) || 0;
|
if (quotaVal <= 0 && adjustMode !== 'override') return;
|
||||||
formApiRef.current?.setValue('quota', current + delta);
|
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 --------------------------- */
|
/* --------------------------- UI --------------------------- */
|
||||||
@@ -305,24 +370,47 @@ const EditUserModal = (props) => {
|
|||||||
|
|
||||||
<Col span={10}>
|
<Col span={10}>
|
||||||
<Form.InputNumber
|
<Form.InputNumber
|
||||||
field='quota'
|
field='quota_amount'
|
||||||
label={t('剩余额度')}
|
label={t('金额')}
|
||||||
placeholder={t('请输入新的剩余额度')}
|
prefix={getCurrencyConfig().symbol}
|
||||||
step={500000}
|
precision={6}
|
||||||
extraText={renderQuotaWithPrompt(values.quota || 0)}
|
step={0.000001}
|
||||||
rules={[{ required: true, message: t('请输入额度') }]}
|
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
readonly
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={14}>
|
<Col span={14}>
|
||||||
<Form.Slot label={t('添加额度')}>
|
<Form.Slot label={t('调整额度')}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconPlus />}
|
icon={<IconEdit />}
|
||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setAdjustModalOpen(true)}
|
||||||
/>
|
>
|
||||||
|
{t('调整额度')}
|
||||||
|
</Button>
|
||||||
</Form.Slot>
|
</Form.Slot>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
<Col span={24}>
|
||||||
|
<div
|
||||||
|
className='text-xs cursor-pointer'
|
||||||
|
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('请输入额度')}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -372,81 +460,102 @@ const EditUserModal = (props) => {
|
|||||||
formApiRef={formApiRef}
|
formApiRef={formApiRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 添加额度模态框 */}
|
{/* 调整额度模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
visible={addQuotaModalOpen}
|
visible={adjustModalOpen}
|
||||||
onOk={() => {
|
onOk={adjustQuota}
|
||||||
addLocalQuota();
|
|
||||||
setIsModalOpen(false);
|
|
||||||
setAddQuotaLocal('');
|
|
||||||
setAddAmountLocal('');
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setIsModalOpen(false);
|
setAdjustModalOpen(false);
|
||||||
|
setAdjustQuotaLocal('');
|
||||||
|
setAdjustAmountLocal('');
|
||||||
|
setAdjustMode('add');
|
||||||
}}
|
}}
|
||||||
|
confirmLoading={adjustLoading}
|
||||||
closable={null}
|
closable={null}
|
||||||
title={
|
title={
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<IconPlus className='mr-2' />
|
<IconEdit className='mr-2' />
|
||||||
{t('添加额度')}
|
{t('调整额度')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
{(() => {
|
<Text type='secondary' className='block mb-2'>
|
||||||
const current = formApiRef.current?.getValue('quota') || 0;
|
{getPreviewText()}
|
||||||
return (
|
</Text>
|
||||||
<Text type='secondary' className='block mb-2'>
|
|
||||||
{`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
{getCurrencyConfig().type !== 'TOKENS' && (
|
<div className='mb-3'>
|
||||||
<div className='mb-3'>
|
<div className='mb-1'>
|
||||||
<div className='mb-1'>
|
<Text size='small'>{t('操作')}</Text>
|
||||||
<Text size='small'>{t('金额')}</Text>
|
|
||||||
<Text size='small' type='tertiary'>
|
|
||||||
{' '}
|
|
||||||
({t('仅用于换算,实际保存的是额度')})
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<InputNumber
|
|
||||||
prefix={getCurrencyConfig().symbol}
|
|
||||||
placeholder={t('输入金额')}
|
|
||||||
value={addAmountLocal}
|
|
||||||
precision={2}
|
|
||||||
onChange={(val) => {
|
|
||||||
setAddAmountLocal(val);
|
|
||||||
setAddQuotaLocal(
|
|
||||||
val != null && val !== ''
|
|
||||||
? displayAmountToQuota(Math.abs(val)) * Math.sign(val)
|
|
||||||
: '',
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
showClear
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<RadioGroup
|
||||||
<div>
|
type='button'
|
||||||
|
value={adjustMode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAdjustMode(e.target.value);
|
||||||
|
setAdjustQuotaLocal('');
|
||||||
|
setAdjustAmountLocal('');
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Radio value='add'>{t('添加')}</Radio>
|
||||||
|
<Radio value='subtract'>{t('减少')}</Radio>
|
||||||
|
<Radio value='override'>{t('覆盖')}</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<div className='mb-1'>
|
||||||
|
<Text size='small'>{t('金额')}</Text>
|
||||||
|
</div>
|
||||||
|
<InputNumber
|
||||||
|
prefix={getCurrencyConfig().symbol}
|
||||||
|
placeholder={t('输入金额')}
|
||||||
|
value={adjustAmountLocal}
|
||||||
|
precision={6}
|
||||||
|
min={adjustMode === 'override' ? undefined : 0}
|
||||||
|
step={0.000001}
|
||||||
|
onChange={(val) => {
|
||||||
|
const amount = val === '' || val == null ? '' : val;
|
||||||
|
setAdjustAmountLocal(amount);
|
||||||
|
setAdjustQuotaLocal(
|
||||||
|
amount === ''
|
||||||
|
? ''
|
||||||
|
: adjustMode === 'override'
|
||||||
|
? displayAmountToQuota(amount)
|
||||||
|
: displayAmountToQuota(Math.abs(amount)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='text-xs cursor-pointer mt-2'
|
||||||
|
style={{ color: 'var(--semi-color-text-2)' }}
|
||||||
|
onClick={() => setShowAdjustQuotaRaw((v) => !v)}
|
||||||
|
>
|
||||||
|
{showAdjustQuotaRaw
|
||||||
|
? `▾ ${t('收起原生额度输入')}`
|
||||||
|
: `▸ ${t('使用原生额度输入')}`}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: showAdjustQuotaRaw ? 'block' : 'none' }} className='mt-2'>
|
||||||
<div className='mb-1'>
|
<div className='mb-1'>
|
||||||
<Text size='small'>{t('额度')}</Text>
|
<Text size='small'>{t('额度')}</Text>
|
||||||
</div>
|
</div>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
placeholder={t('输入额度')}
|
placeholder={t('输入额度')}
|
||||||
value={addQuotaLocal}
|
value={adjustQuotaLocal}
|
||||||
|
min={adjustMode === 'override' ? undefined : 0}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setAddQuotaLocal(val);
|
const quota = val === '' || val == null ? '' : val;
|
||||||
setAddAmountLocal(
|
setAdjustQuotaLocal(quota);
|
||||||
val != null && val !== ''
|
setAdjustAmountLocal(
|
||||||
? Number(
|
quota === ''
|
||||||
(
|
? ''
|
||||||
quotaToDisplayAmount(Math.abs(val)) * Math.sign(val)
|
: adjustMode === 'override'
|
||||||
).toFixed(2),
|
? Number(quotaToDisplayAmount(quota).toFixed(6))
|
||||||
)
|
: Number(quotaToDisplayAmount(Math.abs(quota)).toFixed(6)),
|
||||||
: '',
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
|||||||
Vendored
+29
-7
@@ -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 <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 = () => {
|
|||||||
|
|
||||||
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());
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+10
@@ -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 @@
|
|||||||
"添加键值对": "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",
|
||||||
|
|||||||
Vendored
+10
@@ -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 @@
|
|||||||
"添加键值对": "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",
|
||||||
|
|||||||
Vendored
+10
@@ -812,6 +812,8 @@
|
|||||||
"原密码": "現在のパスワード",
|
"原密码": "現在のパスワード",
|
||||||
"原生格式": "ネイティブ形式",
|
"原生格式": "ネイティブ形式",
|
||||||
"原生额度": "生クォータ",
|
"原生额度": "生クォータ",
|
||||||
|
"使用原生额度输入": "生クォータで入力",
|
||||||
|
"收起原生额度输入": "生クォータ入力を非表示",
|
||||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "重複排除完了:重複排除前 {{before}} 個のAPIキー、重複排除後 {{after}} 個のAPIキー",
|
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "重複排除完了:重複排除前 {{before}} 個のAPIキー、重複排除後 {{after}} 個のAPIキー",
|
||||||
"参与官方同步": "公式との同期",
|
"参与官方同步": "公式との同期",
|
||||||
"参数": "パラメータ",
|
"参数": "パラメータ",
|
||||||
@@ -2127,6 +2129,14 @@
|
|||||||
"添加键值对": "キー/値ペア追加",
|
"添加键值对": "キー/値ペア追加",
|
||||||
"添加问答": "FAQ追加",
|
"添加问答": "FAQ追加",
|
||||||
"添加额度": "残高追加",
|
"添加额度": "残高追加",
|
||||||
|
"减少": "減少",
|
||||||
|
"覆盖": "上書き",
|
||||||
|
"调整额度": "残高調整",
|
||||||
|
"调整额度成功": "残高の調整に成功しました",
|
||||||
|
"当前额度": "現在の残高",
|
||||||
|
"变更": "変更",
|
||||||
|
"预计结果": "予想結果",
|
||||||
|
"正数为增加,负数为减少": "正の数で追加、負の数で減少",
|
||||||
"清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ",
|
"清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ",
|
||||||
"清理失败": "クリーンアップに失敗しました",
|
"清理失败": "クリーンアップに失敗しました",
|
||||||
"清理方式": "クリーンアップモード",
|
"清理方式": "クリーンアップモード",
|
||||||
|
|||||||
Vendored
+10
@@ -827,6 +827,8 @@
|
|||||||
"原密码": "Старый пароль",
|
"原密码": "Старый пароль",
|
||||||
"原生格式": "Нативный формат",
|
"原生格式": "Нативный формат",
|
||||||
"原生额度": "Исходный лимит",
|
"原生额度": "Исходный лимит",
|
||||||
|
"使用原生额度输入": "Ввод в исходных единицах",
|
||||||
|
"收起原生额度输入": "Скрыть ввод в исходных единицах",
|
||||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей",
|
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей",
|
||||||
"参与官方同步": "Участвовать в официальной синхронизации",
|
"参与官方同步": "Участвовать в официальной синхронизации",
|
||||||
"参数": "Параметры",
|
"参数": "Параметры",
|
||||||
@@ -2156,6 +2158,14 @@
|
|||||||
"添加键值对": "Добавить пару ключ-значение",
|
"添加键值对": "Добавить пару ключ-значение",
|
||||||
"添加问答": "Добавить вопрос-ответ",
|
"添加问答": "Добавить вопрос-ответ",
|
||||||
"添加额度": "Добавить лимит",
|
"添加额度": "Добавить лимит",
|
||||||
|
"减少": "Уменьшить",
|
||||||
|
"覆盖": "Заменить",
|
||||||
|
"调整额度": "Скорректировать квоту",
|
||||||
|
"调整额度成功": "Квота успешно скорректирована",
|
||||||
|
"当前额度": "Текущая квота",
|
||||||
|
"变更": "Изменение",
|
||||||
|
"预计结果": "Ожидаемый результат",
|
||||||
|
"正数为增加,负数为减少": "Положительное для увеличения, отрицательное для уменьшения",
|
||||||
"清理不活跃缓存": "Очистить неактивный кэш",
|
"清理不活跃缓存": "Очистить неактивный кэш",
|
||||||
"清理失败": "Ошибка очистки",
|
"清理失败": "Ошибка очистки",
|
||||||
"清理方式": "Режим очистки",
|
"清理方式": "Режим очистки",
|
||||||
|
|||||||
Vendored
+10
@@ -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 @@
|
|||||||
"添加键值对": "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ử",
|
||||||
|
|||||||
Vendored
+10
@@ -1605,6 +1605,14 @@
|
|||||||
"添加键值对": "添加键值对",
|
"添加键值对": "添加键值对",
|
||||||
"添加问答": "添加问答",
|
"添加问答": "添加问答",
|
||||||
"添加额度": "添加额度",
|
"添加额度": "添加额度",
|
||||||
|
"减少": "减少",
|
||||||
|
"覆盖": "覆盖",
|
||||||
|
"调整额度": "调整额度",
|
||||||
|
"调整额度成功": "调整额度成功",
|
||||||
|
"当前额度": "当前额度",
|
||||||
|
"变更": "变更",
|
||||||
|
"预计结果": "预计结果",
|
||||||
|
"正数为增加,负数为减少": "正数为增加,负数为减少",
|
||||||
"清理方式": "清理方式",
|
"清理方式": "清理方式",
|
||||||
"清理日志文件": "清理日志文件",
|
"清理日志文件": "清理日志文件",
|
||||||
"清空": "清空",
|
"清空": "清空",
|
||||||
@@ -2737,6 +2745,8 @@
|
|||||||
"请输入总额度": "请输入总额度",
|
"请输入总额度": "请输入总额度",
|
||||||
"0 表示不限": "0 表示不限",
|
"0 表示不限": "0 表示不限",
|
||||||
"原生额度": "原生额度",
|
"原生额度": "原生额度",
|
||||||
|
"使用原生额度输入": "使用原生额度输入",
|
||||||
|
"收起原生额度输入": "收起原生额度输入",
|
||||||
"升级分组": "升级分组",
|
"升级分组": "升级分组",
|
||||||
"不升级": "不升级",
|
"不升级": "不升级",
|
||||||
"购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。": "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。",
|
"购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。": "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。",
|
||||||
|
|||||||
Vendored
+10
@@ -719,6 +719,8 @@
|
|||||||
"原密码": "原密碼",
|
"原密码": "原密碼",
|
||||||
"原生格式": "原生格式",
|
"原生格式": "原生格式",
|
||||||
"原生额度": "原生額度",
|
"原生额度": "原生額度",
|
||||||
|
"使用原生额度输入": "使用原生額度輸入",
|
||||||
|
"收起原生额度输入": "收起原生額度輸入",
|
||||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 個密鑰,去重後 {{after}} 個密鑰",
|
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 個密鑰,去重後 {{after}} 個密鑰",
|
||||||
"参与官方同步": "參與官方同步",
|
"参与官方同步": "參與官方同步",
|
||||||
"参数": "參數",
|
"参数": "參數",
|
||||||
@@ -1905,6 +1907,14 @@
|
|||||||
"添加键值对": "添加鍵值對",
|
"添加键值对": "添加鍵值對",
|
||||||
"添加问答": "添加問答",
|
"添加问答": "添加問答",
|
||||||
"添加额度": "添加額度",
|
"添加额度": "添加額度",
|
||||||
|
"减少": "減少",
|
||||||
|
"覆盖": "覆蓋",
|
||||||
|
"调整额度": "調整額度",
|
||||||
|
"调整额度成功": "調整額度成功",
|
||||||
|
"当前额度": "當前額度",
|
||||||
|
"变更": "變更",
|
||||||
|
"预计结果": "預計結果",
|
||||||
|
"正数为增加,负数为减少": "正數為增加,負數為減少",
|
||||||
"清理不活跃缓存": "清理不活躍快取",
|
"清理不活跃缓存": "清理不活躍快取",
|
||||||
"清理失败": "清理失敗",
|
"清理失败": "清理失敗",
|
||||||
"清理方式": "清理方式",
|
"清理方式": "清理方式",
|
||||||
|
|||||||
Reference in New Issue
Block a user