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:
CaIon
2026-04-09 22:44:53 +08:00
parent 0664bb3f65
commit 040e8c1da8
21 changed files with 493 additions and 149 deletions
@@ -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) => {
</div>
<Row gutter={12}>
<Col span={12}>
<Form.AutoComplete
field='quota'
label={t('额')}
placeholder={t('请输入额度')}
<Col span={24}>
<Form.InputNumber
field='amount'
label={t('额')}
prefix={getCurrencyConfig().symbol}
placeholder={t('输入金额')}
precision={6}
min={0}
step={0.000001}
style={{ width: '100%' }}
type='number'
rules={[
{ required: true, message: t('请输入额度') },
{
validator: (rule, v) => {
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
/>
<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>
{!isEdit && (
<Col span={12}>