💄refactor: enhance EditUser and AddUser form validation & UX
Changes in `web/src/pages/User/EditUser.js`:
• Added `rules` to
– `Form.Select group`: now required with error “Please select group”.
– `Form.InputNumber quota`: now required with error “Please enter quota”.
• Added `step={500000}` to quota `InputNumber` for quicker numeric input.
• Replaced invalid `readonly` with React-correct `readOnly`, and added descriptive placeholders for all binding-info fields (GitHub/OIDC/WeChat/Email/Telegram).
• Removed unused `downloadTextAsFile` import.
These updates tighten form validation, improve data entry ergonomics, and restore clear read-only indicators for third-party bindings.
This commit is contained in:
@@ -967,7 +967,7 @@ const PersonalSetting = () => {
|
||||
{systemToken && (
|
||||
<div className="mt-3">
|
||||
<Input
|
||||
readOnly
|
||||
readonly
|
||||
value={systemToken}
|
||||
onClick={handleSystemTokenClick}
|
||||
size="large"
|
||||
|
||||
@@ -471,10 +471,11 @@
|
||||
"请输入新的密码": "Please enter a new password",
|
||||
"显示名称": "Display Name",
|
||||
"请输入新的显示名称": "Please enter a new display name",
|
||||
"已绑定的 GitHub 账户": "GitHub Account Bound",
|
||||
"此项只读,要用户通过个人设置页面的相关绑<EFBFBD><EFBFBD>按钮进<EFBFBD><EFBFBD>绑<EFBFBD><EFBFBD><EFBFBD>,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
|
||||
"已绑定的微信账户": "WeChat Account Bound",
|
||||
"已绑定的邮箱账户": "Email Account Bound",
|
||||
"已绑定的 GITHUB 账户": "Bound GitHub Account",
|
||||
"已绑定的 WECHAT 账户": "Bound WeChat Account",
|
||||
"已绑定的 EMAIL 账户": "Bound Email Account",
|
||||
"已绑定的 TELEGRAM 账户": "Bound Telegram Account",
|
||||
"此项只读,要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
|
||||
"用户信息更新成功!": "User information updated successfully!",
|
||||
"使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
|
||||
"用户名称": "User Name",
|
||||
|
||||
@@ -130,7 +130,7 @@ const Home = () => {
|
||||
{/* BASE URL 与端点选择 */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-4 w-full mt-4 md:mt-6 max-w-md">
|
||||
<Input
|
||||
readOnly
|
||||
readonly
|
||||
value={serverAddress}
|
||||
className="flex-1 !rounded-full"
|
||||
size={isMobile() ? 'default' : 'large'}
|
||||
|
||||
@@ -931,7 +931,7 @@ const TopUp = () => {
|
||||
<Title heading={6}>{t('邀请链接')}</Title>
|
||||
<Input
|
||||
value={affLink}
|
||||
readOnly
|
||||
readonly
|
||||
size='large'
|
||||
suffix={
|
||||
<Button
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { API, isMobile, showError, showSuccess } from '../../helpers';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
SideSheet,
|
||||
Space,
|
||||
Spin,
|
||||
Typography,
|
||||
Card,
|
||||
Tag,
|
||||
Avatar
|
||||
Avatar,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconSave,
|
||||
@@ -22,32 +24,23 @@ const { Text, Title } = Typography;
|
||||
|
||||
const AddUser = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const originInputs = {
|
||||
const formApiRef = useRef(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getInitValues = () => ({
|
||||
username: '',
|
||||
display_name: '',
|
||||
password: '',
|
||||
remark: '',
|
||||
};
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { username, display_name, password, remark } = inputs;
|
||||
});
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const submit = async (values) => {
|
||||
setLoading(true);
|
||||
if (inputs.username === '' || inputs.password === '') {
|
||||
setLoading(false);
|
||||
showError(t('用户名和密码不能为空!'));
|
||||
return;
|
||||
}
|
||||
const res = await API.post(`/api/user/`, inputs);
|
||||
const res = await API.post(`/api/user/`, values);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('用户账户创建成功!'));
|
||||
setInputs(originInputs);
|
||||
formApiRef.current?.setValues(getInitValues());
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
} else {
|
||||
@@ -85,7 +78,7 @@ const AddUser = (props) => {
|
||||
<Button
|
||||
theme="solid"
|
||||
className="!rounded-full"
|
||||
onClick={submit}
|
||||
onClick={() => formApiRef.current?.submitForm()}
|
||||
icon={<IconSave />}
|
||||
loading={loading}
|
||||
>
|
||||
@@ -107,71 +100,60 @@ const AddUser = (props) => {
|
||||
onCancel={() => handleCancel()}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div className="p-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">
|
||||
<IconUserAdd 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="off"
|
||||
className="!rounded-lg"
|
||||
showClear
|
||||
required
|
||||
/>
|
||||
<Form
|
||||
initValues={getInitValues()}
|
||||
getFormApi={(api) => formApiRef.current = api}
|
||||
onSubmit={submit}
|
||||
onSubmitFail={(errs) => {
|
||||
const first = Object.values(errs)[0];
|
||||
if (first) showError(Array.isArray(first) ? first[0] : first);
|
||||
formApiRef.current?.scrollToError();
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<IconUserAdd 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('display_name', value)}
|
||||
value={display_name}
|
||||
autoComplete="off"
|
||||
className="!rounded-lg"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong className="block mb-2">{t('密码')}</Text>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('请输入密码')}
|
||||
onChange={(value) => handleInputChange('password', value)}
|
||||
value={password}
|
||||
autoComplete="off"
|
||||
className="!rounded-lg"
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='username'
|
||||
label={t('用户名')}
|
||||
placeholder={t('请输入用户名')}
|
||||
rules={[{ required: true, message: t('请输入用户名') }]} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='display_name'
|
||||
label={t('显示名称')}
|
||||
placeholder={t('请输入显示名称')} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='password'
|
||||
label={t('密码')}
|
||||
type='password'
|
||||
placeholder={t('请输入密码')}
|
||||
rules={[{ required: true, message: t('请输入密码') }]} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='remark'
|
||||
label={t('备注')}
|
||||
placeholder={t('请输入备注(仅管理员可见)')} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
</>
|
||||
|
||||
@@ -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 {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
SideSheet,
|
||||
Space,
|
||||
Spin,
|
||||
Typography,
|
||||
Card,
|
||||
Tag,
|
||||
Form,
|
||||
Avatar,
|
||||
Row,
|
||||
Col,
|
||||
Input,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconUser,
|
||||
@@ -22,73 +31,55 @@ import {
|
||||
IconUserGroup,
|
||||
IconPlus,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const EditUser = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const userId = props.editingUser.id;
|
||||
const [loading, setLoading] = useState(true);
|
||||
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: '',
|
||||
display_name: '',
|
||||
password: '',
|
||||
github_id: '',
|
||||
oidc_id: '',
|
||||
wechat_id: '',
|
||||
telegram_id: '',
|
||||
email: '',
|
||||
quota: 0,
|
||||
group: 'default',
|
||||
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 () => {
|
||||
try {
|
||||
let res = await API.get(`/api/group/`);
|
||||
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 () => {
|
||||
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;
|
||||
if (success) {
|
||||
data.password = '';
|
||||
setInputs(data);
|
||||
formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -96,27 +87,23 @@ const EditUser = (props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser().then();
|
||||
if (userId) {
|
||||
fetchGroups().then();
|
||||
}
|
||||
loadUser();
|
||||
if (userId) fetchGroups();
|
||||
}, [props.editingUser.id]);
|
||||
|
||||
const submit = async () => {
|
||||
/* ----------------------- submit ----------------------- */
|
||||
const submit = async (values) => {
|
||||
setLoading(true);
|
||||
let res = undefined;
|
||||
let payload = { ...values };
|
||||
if (typeof payload.quota === 'string') payload.quota = parseInt(payload.quota) || 0;
|
||||
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;
|
||||
if (success) {
|
||||
showSuccess('用户信息更新成功!');
|
||||
showSuccess(t('用户信息更新成功!'));
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
} else {
|
||||
@@ -125,53 +112,48 @@ const EditUser = (props) => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
/* --------------------- quota helper -------------------- */
|
||||
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 (
|
||||
<>
|
||||
<SideSheet
|
||||
placement={'right'}
|
||||
placement='right'
|
||||
title={
|
||||
<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>
|
||||
</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}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
footer={
|
||||
<div className="flex justify-end bg-white">
|
||||
<div className='flex justify-end bg-white'>
|
||||
<Space>
|
||||
<Button
|
||||
theme="solid"
|
||||
className="!rounded-full"
|
||||
onClick={submit}
|
||||
theme='solid'
|
||||
className='!rounded-full'
|
||||
onClick={() => formApiRef.current?.submitForm()}
|
||||
icon={<IconSave />}
|
||||
loading={loading}
|
||||
>
|
||||
{t('提交')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
className="!rounded-full"
|
||||
type="primary"
|
||||
theme='light'
|
||||
className='!rounded-full'
|
||||
type='primary'
|
||||
onClick={handleCancel}
|
||||
icon={<IconClose />}
|
||||
>
|
||||
@@ -181,215 +163,154 @@ const EditUser = (props) => {
|
||||
</div>
|
||||
}
|
||||
closeIcon={null}
|
||||
onCancel={() => handleCancel()}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{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>
|
||||
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<Text strong>{t('剩余额度')}</Text>
|
||||
<Text type="tertiary">{renderQuotaWithPrompt(quota)}</Text>
|
||||
<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 className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('请输入新的剩余额度')}
|
||||
onChange={(value) => handleInputChange('quota', value)}
|
||||
value={quota}
|
||||
type="number"
|
||||
autoComplete="new-password"
|
||||
className="flex-1 !rounded-lg"
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='username'
|
||||
label={t('用户名')}
|
||||
placeholder={t('请输入新的用户名')}
|
||||
rules={[{ required: true, message: t('请输入用户名') }]}
|
||||
showClear
|
||||
/>
|
||||
<Button
|
||||
onClick={openAddQuotaModal}
|
||||
className="!rounded-lg"
|
||||
icon={<IconPlus />}
|
||||
>
|
||||
{t('添加额度')}
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='password'
|
||||
label={t('密码')}
|
||||
placeholder={t('请输入新的密码,最短 8 位')}
|
||||
mode='password'
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='display_name'
|
||||
label={t('显示名称')}
|
||||
placeholder={t('请输入新的显示名称')}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Input
|
||||
field='remark'
|
||||
label={t('备注')}
|
||||
placeholder={t('请输入备注(仅管理员可见)')}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 权限设置 */}
|
||||
{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>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
field='group'
|
||||
label={t('分组')}
|
||||
placeholder={t('请选择分组')}
|
||||
optionList={groupOptions}
|
||||
allowAdditions
|
||||
search
|
||||
rules={[{ required: true, message: t('请选择分组') }]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<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>
|
||||
|
||||
<Col span={14}>
|
||||
<Form.Slot label={t('添加额度')}>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>
|
||||
</Form.Slot>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 绑定信息 */}
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<Text strong className="block mb-2">{t('已绑定的 OIDC 账户')}</Text>
|
||||
<Input
|
||||
value={oidc_id}
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong className="block mb-2">{t('已绑定的微信账户')}</Text>
|
||||
<Input
|
||||
value={wechat_id}
|
||||
autoComplete="new-password"
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong className="block mb-2">{t('已绑定的邮箱账户')}</Text>
|
||||
<Input
|
||||
value={email}
|
||||
autoComplete="new-password"
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
|
||||
{/* 添加额度模态框 */}
|
||||
<Modal
|
||||
centered={true}
|
||||
centered
|
||||
visible={addQuotaModalOpen}
|
||||
onOk={() => {
|
||||
addLocalQuota();
|
||||
@@ -398,26 +319,30 @@ const EditUser = (props) => {
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
closable={null}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconPlus className="mr-2" />
|
||||
<div className='flex items-center'>
|
||||
<IconPlus className='mr-2' />
|
||||
{t('添加额度')}
|
||||
</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>
|
||||
<Input
|
||||
placeholder={t('需要添加的额度(支持负数)')}
|
||||
onChange={(value) => {
|
||||
setAddQuotaLocal(value);
|
||||
}}
|
||||
type='number'
|
||||
value={addQuotaLocal}
|
||||
type="number"
|
||||
autoComplete="new-password"
|
||||
className="!rounded-lg"
|
||||
onChange={setAddQuotaLocal}
|
||||
showClear
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user