|
@@ -1,18 +1,27 @@
|
|
|
-import React, { useEffect, useState } from 'react';
|
|
|
|
|
-import { useNavigate } from 'react-router-dom';
|
|
|
|
|
-import { API, isMobile, showError, showSuccess, renderQuota, renderQuotaWithPrompt } from '../../helpers';
|
|
|
|
|
|
|
+import React, { useEffect, useState, useRef } from 'react';
|
|
|
|
|
+import { useTranslation } from 'react-i18next';
|
|
|
|
|
+import {
|
|
|
|
|
+ API,
|
|
|
|
|
+ isMobile,
|
|
|
|
|
+ showError,
|
|
|
|
|
+ showSuccess,
|
|
|
|
|
+ renderQuota,
|
|
|
|
|
+ renderQuotaWithPrompt,
|
|
|
|
|
+} from '../../helpers';
|
|
|
import {
|
|
import {
|
|
|
Button,
|
|
Button,
|
|
|
- Input,
|
|
|
|
|
Modal,
|
|
Modal,
|
|
|
- Select,
|
|
|
|
|
SideSheet,
|
|
SideSheet,
|
|
|
Space,
|
|
Space,
|
|
|
Spin,
|
|
Spin,
|
|
|
Typography,
|
|
Typography,
|
|
|
Card,
|
|
Card,
|
|
|
Tag,
|
|
Tag,
|
|
|
|
|
+ Form,
|
|
|
Avatar,
|
|
Avatar,
|
|
|
|
|
+ Row,
|
|
|
|
|
+ Col,
|
|
|
|
|
+ Input,
|
|
|
} from '@douyinfe/semi-ui';
|
|
} from '@douyinfe/semi-ui';
|
|
|
import {
|
|
import {
|
|
|
IconUser,
|
|
IconUser,
|
|
@@ -22,73 +31,55 @@ import {
|
|
|
IconUserGroup,
|
|
IconUserGroup,
|
|
|
IconPlus,
|
|
IconPlus,
|
|
|
} from '@douyinfe/semi-icons';
|
|
} from '@douyinfe/semi-icons';
|
|
|
-import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
|
|
|
|
const { Text, Title } = Typography;
|
|
const { Text, Title } = Typography;
|
|
|
|
|
|
|
|
const EditUser = (props) => {
|
|
const EditUser = (props) => {
|
|
|
|
|
+ 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 [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
|
|
- const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
|
|
|
|
- const [inputs, setInputs] = useState({
|
|
|
|
|
|
|
+ const [addQuotaLocal, setAddQuotaLocal] = useState('0');
|
|
|
|
|
+ const [groupOptions, setGroupOptions] = useState([]);
|
|
|
|
|
+ const formApiRef = useRef(null);
|
|
|
|
|
+
|
|
|
|
|
+ const isEdit = Boolean(userId);
|
|
|
|
|
+
|
|
|
|
|
+ const getInitValues = () => ({
|
|
|
username: '',
|
|
username: '',
|
|
|
display_name: '',
|
|
display_name: '',
|
|
|
password: '',
|
|
password: '',
|
|
|
github_id: '',
|
|
github_id: '',
|
|
|
oidc_id: '',
|
|
oidc_id: '',
|
|
|
wechat_id: '',
|
|
wechat_id: '',
|
|
|
|
|
+ telegram_id: '',
|
|
|
email: '',
|
|
email: '',
|
|
|
quota: 0,
|
|
quota: 0,
|
|
|
group: 'default',
|
|
group: 'default',
|
|
|
remark: '',
|
|
remark: '',
|
|
|
});
|
|
});
|
|
|
- const [groupOptions, setGroupOptions] = useState([]);
|
|
|
|
|
- const {
|
|
|
|
|
- username,
|
|
|
|
|
- display_name,
|
|
|
|
|
- password,
|
|
|
|
|
- github_id,
|
|
|
|
|
- oidc_id,
|
|
|
|
|
- wechat_id,
|
|
|
|
|
- telegram_id,
|
|
|
|
|
- email,
|
|
|
|
|
- quota,
|
|
|
|
|
- group,
|
|
|
|
|
- remark,
|
|
|
|
|
- } = inputs;
|
|
|
|
|
- const handleInputChange = (name, value) => {
|
|
|
|
|
- setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
|
|
|
- };
|
|
|
|
|
|
|
+
|
|
|
const fetchGroups = async () => {
|
|
const fetchGroups = async () => {
|
|
|
try {
|
|
try {
|
|
|
let res = await API.get(`/api/group/`);
|
|
let res = await API.get(`/api/group/`);
|
|
|
setGroupOptions(
|
|
setGroupOptions(
|
|
|
- res.data.data.map((group) => ({
|
|
|
|
|
- label: group,
|
|
|
|
|
- value: group,
|
|
|
|
|
- })),
|
|
|
|
|
|
|
+ res.data.data.map((g) => ({ label: g, value: g }))
|
|
|
);
|
|
);
|
|
|
- } catch (error) {
|
|
|
|
|
- showError(error.message);
|
|
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ showError(e.message);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
- const navigate = useNavigate();
|
|
|
|
|
- const handleCancel = () => {
|
|
|
|
|
- props.handleClose();
|
|
|
|
|
- };
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const handleCancel = () => props.handleClose();
|
|
|
|
|
+
|
|
|
const loadUser = async () => {
|
|
const loadUser = async () => {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
- let res = undefined;
|
|
|
|
|
- if (userId) {
|
|
|
|
|
- res = await API.get(`/api/user/${userId}`);
|
|
|
|
|
- } else {
|
|
|
|
|
- res = await API.get(`/api/user/self`);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const url = userId ? `/api/user/${userId}` : `/api/user/self`;
|
|
|
|
|
+ const res = await API.get(url);
|
|
|
const { success, message, data } = res.data;
|
|
const { success, message, data } = res.data;
|
|
|
if (success) {
|
|
if (success) {
|
|
|
data.password = '';
|
|
data.password = '';
|
|
|
- setInputs(data);
|
|
|
|
|
|
|
+ formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
|
|
} else {
|
|
} else {
|
|
|
showError(message);
|
|
showError(message);
|
|
|
}
|
|
}
|
|
@@ -96,27 +87,23 @@ const EditUser = (props) => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- loadUser().then();
|
|
|
|
|
- if (userId) {
|
|
|
|
|
- fetchGroups().then();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ loadUser();
|
|
|
|
|
+ if (userId) fetchGroups();
|
|
|
}, [props.editingUser.id]);
|
|
}, [props.editingUser.id]);
|
|
|
|
|
|
|
|
- const submit = async () => {
|
|
|
|
|
|
|
+ /* ----------------------- submit ----------------------- */
|
|
|
|
|
+ const submit = async (values) => {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
- let res = undefined;
|
|
|
|
|
|
|
+ let payload = { ...values };
|
|
|
|
|
+ if (typeof payload.quota === 'string') payload.quota = parseInt(payload.quota) || 0;
|
|
|
if (userId) {
|
|
if (userId) {
|
|
|
- let data = { ...inputs, id: parseInt(userId) };
|
|
|
|
|
- if (typeof data.quota === 'string') {
|
|
|
|
|
- data.quota = parseInt(data.quota);
|
|
|
|
|
- }
|
|
|
|
|
- res = await API.put(`/api/user/`, data);
|
|
|
|
|
- } else {
|
|
|
|
|
- res = await API.put(`/api/user/self`, inputs);
|
|
|
|
|
|
|
+ payload.id = parseInt(userId);
|
|
|
}
|
|
}
|
|
|
|
|
+ const url = userId ? `/api/user/` : `/api/user/self`;
|
|
|
|
|
+ const res = await API.put(url, payload);
|
|
|
const { success, message } = res.data;
|
|
const { success, message } = res.data;
|
|
|
if (success) {
|
|
if (success) {
|
|
|
- showSuccess('用户信息更新成功!');
|
|
|
|
|
|
|
+ showSuccess(t('用户信息更新成功!'));
|
|
|
props.refresh();
|
|
props.refresh();
|
|
|
props.handleClose();
|
|
props.handleClose();
|
|
|
} else {
|
|
} else {
|
|
@@ -125,53 +112,48 @@ const EditUser = (props) => {
|
|
|
setLoading(false);
|
|
setLoading(false);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /* --------------------- quota helper -------------------- */
|
|
|
const addLocalQuota = () => {
|
|
const addLocalQuota = () => {
|
|
|
- let newQuota = parseInt(quota) + parseInt(addQuotaLocal);
|
|
|
|
|
- setInputs((inputs) => ({ ...inputs, quota: newQuota }));
|
|
|
|
|
|
|
+ const current = parseInt(formApiRef.current?.getValue('quota') || 0);
|
|
|
|
|
+ const delta = parseInt(addQuotaLocal) || 0;
|
|
|
|
|
+ formApiRef.current?.setValue('quota', current + delta);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const openAddQuotaModal = () => {
|
|
|
|
|
- setAddQuotaLocal('0');
|
|
|
|
|
- setIsModalOpen(true);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const { t } = useTranslation();
|
|
|
|
|
-
|
|
|
|
|
|
|
+ /* --------------------------- UI --------------------------- */
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
|
<SideSheet
|
|
<SideSheet
|
|
|
- placement={'right'}
|
|
|
|
|
|
|
+ placement='right'
|
|
|
title={
|
|
title={
|
|
|
<Space>
|
|
<Space>
|
|
|
- <Tag color="blue" shape="circle">{t('编辑')}</Tag>
|
|
|
|
|
- <Title heading={4} className="m-0">
|
|
|
|
|
- {t('编辑用户')}
|
|
|
|
|
|
|
+ <Tag color='blue' shape='circle'>
|
|
|
|
|
+ {t(isEdit ? '编辑' : '新建')}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ <Title heading={4} className='m-0'>
|
|
|
|
|
+ {isEdit ? t('编辑用户') : t('创建用户')}
|
|
|
</Title>
|
|
</Title>
|
|
|
</Space>
|
|
</Space>
|
|
|
}
|
|
}
|
|
|
- headerStyle={{
|
|
|
|
|
- borderBottom: '1px solid var(--semi-color-border)',
|
|
|
|
|
- padding: '24px'
|
|
|
|
|
- }}
|
|
|
|
|
- bodyStyle={{ padding: '0' }}
|
|
|
|
|
|
|
+ headerStyle={{ borderBottom: '1px solid var(--semi-color-border)', padding: '24px' }}
|
|
|
|
|
+ bodyStyle={{ padding: 0 }}
|
|
|
visible={props.visible}
|
|
visible={props.visible}
|
|
|
width={isMobile() ? '100%' : 600}
|
|
width={isMobile() ? '100%' : 600}
|
|
|
footer={
|
|
footer={
|
|
|
- <div className="flex justify-end bg-white">
|
|
|
|
|
|
|
+ <div className='flex justify-end bg-white'>
|
|
|
<Space>
|
|
<Space>
|
|
|
<Button
|
|
<Button
|
|
|
- theme="solid"
|
|
|
|
|
- className="!rounded-full"
|
|
|
|
|
- onClick={submit}
|
|
|
|
|
|
|
+ theme='solid'
|
|
|
|
|
+ className='!rounded-full'
|
|
|
|
|
+ onClick={() => formApiRef.current?.submitForm()}
|
|
|
icon={<IconSave />}
|
|
icon={<IconSave />}
|
|
|
loading={loading}
|
|
loading={loading}
|
|
|
>
|
|
>
|
|
|
{t('提交')}
|
|
{t('提交')}
|
|
|
</Button>
|
|
</Button>
|
|
|
<Button
|
|
<Button
|
|
|
- theme="light"
|
|
|
|
|
- className="!rounded-full"
|
|
|
|
|
- type="primary"
|
|
|
|
|
|
|
+ theme='light'
|
|
|
|
|
+ className='!rounded-full'
|
|
|
|
|
+ type='primary'
|
|
|
onClick={handleCancel}
|
|
onClick={handleCancel}
|
|
|
icon={<IconClose />}
|
|
icon={<IconClose />}
|
|
|
>
|
|
>
|
|
@@ -181,215 +163,154 @@ const EditUser = (props) => {
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
closeIcon={null}
|
|
closeIcon={null}
|
|
|
- onCancel={() => handleCancel()}
|
|
|
|
|
|
|
+ onCancel={handleCancel}
|
|
|
>
|
|
>
|
|
|
<Spin spinning={loading}>
|
|
<Spin spinning={loading}>
|
|
|
- <div className="p-6">
|
|
|
|
|
- <Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
|
|
|
- {/* Header: Basic Info */}
|
|
|
|
|
- <div className="flex items-center mb-2">
|
|
|
|
|
- <Avatar size="small" color="blue" className="mr-2 shadow-md">
|
|
|
|
|
- <IconUser size={16} />
|
|
|
|
|
- </Avatar>
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text className="text-lg font-medium">{t('基本信息')}</Text>
|
|
|
|
|
- <div className="text-xs text-gray-600">{t('用户的基本账户信息')}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('用户名')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- placeholder={t('请输入新的用户名')}
|
|
|
|
|
- onChange={(value) => handleInputChange('username', value)}
|
|
|
|
|
- value={username}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('密码')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- type="password"
|
|
|
|
|
- placeholder={t('请输入新的密码,最短 8 位')}
|
|
|
|
|
- onChange={(value) => handleInputChange('password', value)}
|
|
|
|
|
- value={password}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('显示名称')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- placeholder={t('请输入新的显示名称')}
|
|
|
|
|
- onChange={(value) => handleInputChange('display_name', value)}
|
|
|
|
|
- value={display_name}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Form
|
|
|
|
|
+ initValues={getInitValues()}
|
|
|
|
|
+ getFormApi={(api) => (formApiRef.current = api)}
|
|
|
|
|
+ onSubmit={submit}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ values }) => (
|
|
|
|
|
+ <div className='p-6 space-y-6'>
|
|
|
|
|
+ {/* 基本信息 */}
|
|
|
|
|
+ <Card className='!rounded-2xl shadow-sm border-0'>
|
|
|
|
|
+ <div className='flex items-center mb-2'>
|
|
|
|
|
+ <Avatar size='small' color='blue' className='mr-2 shadow-md'>
|
|
|
|
|
+ <IconUser size={16} />
|
|
|
|
|
+ </Avatar>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Text className='text-lg font-medium'>{t('基本信息')}</Text>
|
|
|
|
|
+ <div className='text-xs text-gray-600'>{t('用户的基本账户信息')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('备注')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- placeholder={t('请输入备注(仅管理员可见)')}
|
|
|
|
|
- onChange={(value) => handleInputChange('remark', value)}
|
|
|
|
|
- value={remark}
|
|
|
|
|
- autoComplete="off"
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </Card>
|
|
|
|
|
|
|
+ <Row gutter={12}>
|
|
|
|
|
+ <Col span={24}>
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='username'
|
|
|
|
|
+ label={t('用户名')}
|
|
|
|
|
+ placeholder={t('请输入新的用户名')}
|
|
|
|
|
+ rules={[{ required: true, message: t('请输入用户名') }]}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ </Col>
|
|
|
|
|
|
|
|
- {userId && (
|
|
|
|
|
- <Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
|
|
|
- {/* Header: Permission Settings */}
|
|
|
|
|
- <div className="flex items-center mb-2">
|
|
|
|
|
- <Avatar size="small" color="green" className="mr-2 shadow-md">
|
|
|
|
|
- <IconUserGroup size={16} />
|
|
|
|
|
- </Avatar>
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text className="text-lg font-medium">{t('权限设置')}</Text>
|
|
|
|
|
- <div className="text-xs text-gray-600">{t('用户分组和额度管理')}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Col span={24}>
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='password'
|
|
|
|
|
+ label={t('密码')}
|
|
|
|
|
+ placeholder={t('请输入新的密码,最短 8 位')}
|
|
|
|
|
+ mode='password'
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ </Col>
|
|
|
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('分组')}</Text>
|
|
|
|
|
- <Select
|
|
|
|
|
- placeholder={t('请选择分组')}
|
|
|
|
|
- search
|
|
|
|
|
- allowAdditions
|
|
|
|
|
- additionLabel={t(
|
|
|
|
|
- '请在系统设置页面编辑分组倍率以添加新的分组:',
|
|
|
|
|
- )}
|
|
|
|
|
- onChange={(value) => handleInputChange('group', value)}
|
|
|
|
|
- value={inputs.group}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- optionList={groupOptions}
|
|
|
|
|
- className="w-full !rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Col span={24}>
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='display_name'
|
|
|
|
|
+ label={t('显示名称')}
|
|
|
|
|
+ placeholder={t('请输入新的显示名称')}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ </Col>
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <div className="flex justify-between mb-2">
|
|
|
|
|
- <Text strong>{t('剩余额度')}</Text>
|
|
|
|
|
- <Text type="tertiary">{renderQuotaWithPrompt(quota)}</Text>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="flex gap-2">
|
|
|
|
|
- <Input
|
|
|
|
|
- placeholder={t('请输入新的剩余额度')}
|
|
|
|
|
- onChange={(value) => handleInputChange('quota', value)}
|
|
|
|
|
- value={quota}
|
|
|
|
|
- type="number"
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- className="flex-1 !rounded-lg"
|
|
|
|
|
|
|
+ <Col span={24}>
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='remark'
|
|
|
|
|
+ label={t('备注')}
|
|
|
|
|
+ placeholder={t('请输入备注(仅管理员可见)')}
|
|
|
|
|
+ showClear
|
|
|
/>
|
|
/>
|
|
|
- <Button
|
|
|
|
|
- onClick={openAddQuotaModal}
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- icon={<IconPlus />}
|
|
|
|
|
- >
|
|
|
|
|
- {t('添加额度')}
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ </Row>
|
|
|
|
|
+ </Card>
|
|
|
|
|
|
|
|
- <Card className="!rounded-2xl shadow-sm border-0">
|
|
|
|
|
- {/* Header: Bindings */}
|
|
|
|
|
- <div className="flex items-center mb-2">
|
|
|
|
|
- <Avatar size="small" color="purple" className="mr-2 shadow-md">
|
|
|
|
|
- <IconLink size={16} />
|
|
|
|
|
- </Avatar>
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text className="text-lg font-medium">{t('绑定信息')}</Text>
|
|
|
|
|
- <div className="text-xs text-gray-600">{t('第三方账户绑定状态(只读)')}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* 权限设置 */}
|
|
|
|
|
+ {userId && (
|
|
|
|
|
+ <Card className='!rounded-2xl shadow-sm border-0'>
|
|
|
|
|
+ <div className='flex items-center mb-2'>
|
|
|
|
|
+ <Avatar size='small' color='green' className='mr-2 shadow-md'>
|
|
|
|
|
+ <IconUserGroup size={16} />
|
|
|
|
|
+ </Avatar>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Text className='text-lg font-medium'>{t('权限设置')}</Text>
|
|
|
|
|
+ <div className='text-xs text-gray-600'>{t('用户分组和额度管理')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('已绑定的 GitHub 账户')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- value={github_id}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- placeholder={t(
|
|
|
|
|
- '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
|
|
|
- )}
|
|
|
|
|
- readonly
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Row gutter={12}>
|
|
|
|
|
+ <Col span={24}>
|
|
|
|
|
+ <Form.Select
|
|
|
|
|
+ field='group'
|
|
|
|
|
+ label={t('分组')}
|
|
|
|
|
+ placeholder={t('请选择分组')}
|
|
|
|
|
+ optionList={groupOptions}
|
|
|
|
|
+ allowAdditions
|
|
|
|
|
+ search
|
|
|
|
|
+ rules={[{ required: true, message: t('请选择分组') }]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Col>
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('已绑定的 OIDC 账户')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- value={oidc_id}
|
|
|
|
|
- placeholder={t(
|
|
|
|
|
- '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
|
|
|
- )}
|
|
|
|
|
- readonly
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Col span={10}>
|
|
|
|
|
+ <Form.InputNumber
|
|
|
|
|
+ field='quota'
|
|
|
|
|
+ label={t('剩余额度')}
|
|
|
|
|
+ placeholder={t('请输入新的剩余额度')}
|
|
|
|
|
+ min={0}
|
|
|
|
|
+ step={500000}
|
|
|
|
|
+ extraText={renderQuotaWithPrompt(values.quota || 0)}
|
|
|
|
|
+ rules={[{ required: true, message: t('请输入额度') }]}
|
|
|
|
|
+ style={{ width: '100%' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Col>
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('已绑定的微信账户')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- value={wechat_id}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- placeholder={t(
|
|
|
|
|
- '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
|
|
|
- )}
|
|
|
|
|
- readonly
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Col span={14}>
|
|
|
|
|
+ <Form.Slot label={t('添加额度')}>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon={<IconPlus />}
|
|
|
|
|
+ onClick={() => setIsModalOpen(true)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Form.Slot>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ </Row>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('已绑定的邮箱账户')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- value={email}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- placeholder={t(
|
|
|
|
|
- '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
|
|
|
- )}
|
|
|
|
|
- readonly
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* 绑定信息 */}
|
|
|
|
|
+ <Card className='!rounded-2xl shadow-sm border-0'>
|
|
|
|
|
+ <div className='flex items-center mb-2'>
|
|
|
|
|
+ <Avatar size='small' color='purple' className='mr-2 shadow-md'>
|
|
|
|
|
+ <IconLink size={16} />
|
|
|
|
|
+ </Avatar>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Text className='text-lg font-medium'>{t('绑定信息')}</Text>
|
|
|
|
|
+ <div className='text-xs text-gray-600'>{t('第三方账户绑定状态(只读)')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <Text strong className="block mb-2">{t('已绑定的 Telegram 账户')}</Text>
|
|
|
|
|
- <Input
|
|
|
|
|
- value={telegram_id}
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- placeholder={t(
|
|
|
|
|
- '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
|
|
|
- )}
|
|
|
|
|
- readonly
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Row gutter={12}>
|
|
|
|
|
+ {['github_id', 'oidc_id', 'wechat_id', 'email', 'telegram_id'].map((field) => (
|
|
|
|
|
+ <Col span={24} key={field}>
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field={field}
|
|
|
|
|
+ label={t(`已绑定的 ${field.replace('_id', '').toUpperCase()} 账户`)}
|
|
|
|
|
+ readonly
|
|
|
|
|
+ placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Row>
|
|
|
|
|
+ </Card>
|
|
|
</div>
|
|
</div>
|
|
|
- </Card>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Form>
|
|
|
</Spin>
|
|
</Spin>
|
|
|
</SideSheet>
|
|
</SideSheet>
|
|
|
|
|
|
|
|
|
|
+ {/* 添加额度模态框 */}
|
|
|
<Modal
|
|
<Modal
|
|
|
- centered={true}
|
|
|
|
|
|
|
+ centered
|
|
|
visible={addQuotaModalOpen}
|
|
visible={addQuotaModalOpen}
|
|
|
onOk={() => {
|
|
onOk={() => {
|
|
|
addLocalQuota();
|
|
addLocalQuota();
|
|
@@ -398,26 +319,30 @@ const EditUser = (props) => {
|
|
|
onCancel={() => setIsModalOpen(false)}
|
|
onCancel={() => setIsModalOpen(false)}
|
|
|
closable={null}
|
|
closable={null}
|
|
|
title={
|
|
title={
|
|
|
- <div className="flex items-center">
|
|
|
|
|
- <IconPlus className="mr-2" />
|
|
|
|
|
|
|
+ <div className='flex items-center'>
|
|
|
|
|
+ <IconPlus className='mr-2' />
|
|
|
{t('添加额度')}
|
|
{t('添加额度')}
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
>
|
|
>
|
|
|
- <div className="mb-4">
|
|
|
|
|
- <Text type="secondary" className="block mb-2">
|
|
|
|
|
- {`${t('新额度')}${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal || 0))}`}
|
|
|
|
|
- </Text>
|
|
|
|
|
|
|
+ <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>
|
|
|
|
|
+ );
|
|
|
|
|
+ })()
|
|
|
|
|
+ }
|
|
|
</div>
|
|
</div>
|
|
|
<Input
|
|
<Input
|
|
|
placeholder={t('需要添加的额度(支持负数)')}
|
|
placeholder={t('需要添加的额度(支持负数)')}
|
|
|
- onChange={(value) => {
|
|
|
|
|
- setAddQuotaLocal(value);
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ type='number'
|
|
|
value={addQuotaLocal}
|
|
value={addQuotaLocal}
|
|
|
- type="number"
|
|
|
|
|
- autoComplete="new-password"
|
|
|
|
|
- className="!rounded-lg"
|
|
|
|
|
|
|
+ onChange={setAddQuotaLocal}
|
|
|
|
|
+ showClear
|
|
|
/>
|
|
/>
|
|
|
</Modal>
|
|
</Modal>
|
|
|
</>
|
|
</>
|