|
|
@@ -24,7 +24,6 @@ import {
|
|
|
showError,
|
|
|
showSuccess,
|
|
|
renderQuota,
|
|
|
- renderQuotaWithPrompt,
|
|
|
getCurrencyConfig,
|
|
|
} from '../../../../helpers';
|
|
|
import {
|
|
|
@@ -46,6 +45,8 @@ import {
|
|
|
Row,
|
|
|
Col,
|
|
|
InputNumber,
|
|
|
+ RadioGroup,
|
|
|
+ Radio,
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
import {
|
|
|
IconUser,
|
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
IconClose,
|
|
|
IconLink,
|
|
|
IconUserGroup,
|
|
|
- IconPlus,
|
|
|
+ IconEdit,
|
|
|
} from '@douyinfe/semi-icons';
|
|
|
import UserBindingManagementModal from './UserBindingManagementModal';
|
|
|
|
|
|
@@ -63,13 +64,18 @@ const EditUserModal = (props) => {
|
|
|
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) => {
|
|
|
|
|
|
<Col span={10}>
|
|
|
<Form.InputNumber
|
|
|
- field='quota'
|
|
|
- label={t('剩余额度')}
|
|
|
- placeholder={t('请输入新的剩余额度')}
|
|
|
- step={500000}
|
|
|
- extraText={renderQuotaWithPrompt(values.quota || 0)}
|
|
|
- rules={[{ required: true, message: t('请输入额度') }]}
|
|
|
+ field='quota_amount'
|
|
|
+ label={t('金额')}
|
|
|
+ prefix={getCurrencyConfig().symbol}
|
|
|
+ precision={6}
|
|
|
+ step={0.000001}
|
|
|
style={{ width: '100%' }}
|
|
|
+ readonly
|
|
|
/>
|
|
|
</Col>
|
|
|
|
|
|
<Col span={14}>
|
|
|
- <Form.Slot label={t('添加额度')}>
|
|
|
+ <Form.Slot label={t('调整额度')}>
|
|
|
<Button
|
|
|
- icon={<IconPlus />}
|
|
|
- onClick={() => setIsModalOpen(true)}
|
|
|
- />
|
|
|
+ icon={<IconEdit />}
|
|
|
+ onClick={() => setAdjustModalOpen(true)}
|
|
|
+ >
|
|
|
+ {t('调整额度')}
|
|
|
+ </Button>
|
|
|
</Form.Slot>
|
|
|
</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>
|
|
|
</Card>
|
|
|
)}
|
|
|
@@ -372,81 +460,102 @@ const EditUserModal = (props) => {
|
|
|
formApiRef={formApiRef}
|
|
|
/>
|
|
|
|
|
|
- {/* 添加额度模态框 */}
|
|
|
+ {/* 调整额度模态框 */}
|
|
|
<Modal
|
|
|
centered
|
|
|
- visible={addQuotaModalOpen}
|
|
|
- onOk={() => {
|
|
|
- addLocalQuota();
|
|
|
- setIsModalOpen(false);
|
|
|
- setAddQuotaLocal('');
|
|
|
- setAddAmountLocal('');
|
|
|
- }}
|
|
|
+ visible={adjustModalOpen}
|
|
|
+ onOk={adjustQuota}
|
|
|
onCancel={() => {
|
|
|
- setIsModalOpen(false);
|
|
|
+ setAdjustModalOpen(false);
|
|
|
+ setAdjustQuotaLocal('');
|
|
|
+ setAdjustAmountLocal('');
|
|
|
+ setAdjustMode('add');
|
|
|
}}
|
|
|
+ confirmLoading={adjustLoading}
|
|
|
closable={null}
|
|
|
title={
|
|
|
<div className='flex items-center'>
|
|
|
- <IconPlus className='mr-2' />
|
|
|
- {t('添加额度')}
|
|
|
+ <IconEdit className='mr-2' />
|
|
|
+ {t('调整额度')}
|
|
|
</div>
|
|
|
}
|
|
|
>
|
|
|
<div className='mb-4'>
|
|
|
- {(() => {
|
|
|
- const current = formApiRef.current?.getValue('quota') || 0;
|
|
|
- return (
|
|
|
- <Text type='secondary' className='block mb-2'>
|
|
|
- {`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
|
|
|
- </Text>
|
|
|
- );
|
|
|
- })()}
|
|
|
+ <Text type='secondary' className='block mb-2'>
|
|
|
+ {getPreviewText()}
|
|
|
+ </Text>
|
|
|
</div>
|
|
|
- {getCurrencyConfig().type !== 'TOKENS' && (
|
|
|
- <div className='mb-3'>
|
|
|
- <div className='mb-1'>
|
|
|
- <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 className='mb-3'>
|
|
|
+ <div className='mb-1'>
|
|
|
+ <Text size='small'>{t('操作')}</Text>
|
|
|
+ </div>
|
|
|
+ <RadioGroup
|
|
|
+ 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>
|
|
|
- )}
|
|
|
- <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'>
|
|
|
<Text size='small'>{t('额度')}</Text>
|
|
|
</div>
|
|
|
<InputNumber
|
|
|
placeholder={t('输入额度')}
|
|
|
- value={addQuotaLocal}
|
|
|
+ value={adjustQuotaLocal}
|
|
|
+ min={adjustMode === 'override' ? undefined : 0}
|
|
|
onChange={(val) => {
|
|
|
- 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%' }}
|