💄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:
t0ng7u
2025-06-27 09:44:18 +08:00
parent ab32e15a86
commit 38d3ab5acf
6 changed files with 290 additions and 382 deletions

View File

@@ -967,7 +967,7 @@ const PersonalSetting = () => {
{systemToken && ( {systemToken && (
<div className="mt-3"> <div className="mt-3">
<Input <Input
readOnly readonly
value={systemToken} value={systemToken}
onClick={handleSystemTokenClick} onClick={handleSystemTokenClick}
size="large" size="large"

View File

@@ -471,10 +471,11 @@
"请输入新的密码": "Please enter a new password", "请输入新的密码": "Please enter a new password",
"显示名称": "Display Name", "显示名称": "Display Name",
"请输入新的显示名称": "Please enter a new display name", "请输入新的显示名称": "Please enter a new display name",
"已绑定的 GitHub 账户": "GitHub Account Bound", "已绑定的 GITHUB 账户": "Bound GitHub Account",
"此项只读要用户通过个人设置页面的相关绑<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 账户": "Bound WeChat Account",
"已绑定的微信账户": "WeChat Account Bound", "已绑定的 EMAIL 账户": "Bound Email Account",
"已绑定的邮箱账户": "Email Account Bound", "已绑定的 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!", "用户信息更新成功!": "User information updated successfully!",
"使用明细(总消耗额度:{renderQuota(stat.quota)}": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})", "使用明细(总消耗额度:{renderQuota(stat.quota)}": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
"用户名称": "User Name", "用户名称": "User Name",

View File

@@ -130,7 +130,7 @@ const Home = () => {
{/* BASE URL 与端点选择 */} {/* 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"> <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 <Input
readOnly readonly
value={serverAddress} value={serverAddress}
className="flex-1 !rounded-full" className="flex-1 !rounded-full"
size={isMobile() ? 'default' : 'large'} size={isMobile() ? 'default' : 'large'}

View File

@@ -931,7 +931,7 @@ const TopUp = () => {
<Title heading={6}>{t('邀请链接')}</Title> <Title heading={6}>{t('邀请链接')}</Title>
<Input <Input
value={affLink} value={affLink}
readOnly readonly
size='large' size='large'
suffix={ suffix={
<Button <Button

View File

@@ -1,15 +1,17 @@
import React, { useState } from 'react'; import React, { useState, useRef } from 'react';
import { API, isMobile, showError, showSuccess } from '../../helpers'; import { API, isMobile, showError, showSuccess } from '../../helpers';
import { import {
Button, Button,
Input,
SideSheet, SideSheet,
Space, Space,
Spin, Spin,
Typography, Typography,
Card, Card,
Tag, Tag,
Avatar Avatar,
Form,
Row,
Col,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconSave, IconSave,
@@ -22,32 +24,23 @@ const { Text, Title } = Typography;
const AddUser = (props) => { const AddUser = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const originInputs = { const formApiRef = useRef(null);
const [loading, setLoading] = useState(false);
const getInitValues = () => ({
username: '', username: '',
display_name: '', display_name: '',
password: '', password: '',
remark: '', remark: '',
}; });
const [inputs, setInputs] = useState(originInputs);
const [loading, setLoading] = useState(false);
const { username, display_name, password, remark } = inputs;
const handleInputChange = (name, value) => { const submit = async (values) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const submit = async () => {
setLoading(true); setLoading(true);
if (inputs.username === '' || inputs.password === '') { const res = await API.post(`/api/user/`, values);
setLoading(false);
showError(t('用户名和密码不能为空!'));
return;
}
const res = await API.post(`/api/user/`, inputs);
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('用户账户创建成功!')); showSuccess(t('用户账户创建成功!'));
setInputs(originInputs); formApiRef.current?.setValues(getInitValues());
props.refresh(); props.refresh();
props.handleClose(); props.handleClose();
} else { } else {
@@ -85,7 +78,7 @@ const AddUser = (props) => {
<Button <Button
theme="solid" theme="solid"
className="!rounded-full" className="!rounded-full"
onClick={submit} onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />} icon={<IconSave />}
loading={loading} loading={loading}
> >
@@ -107,71 +100,60 @@ const AddUser = (props) => {
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<div className="p-6"> <Form
<Card className="!rounded-2xl shadow-sm border-0"> initValues={getInitValues()}
<div className="flex items-center mb-2"> getFormApi={(api) => formApiRef.current = api}
<Avatar size="small" color="blue" className="mr-2 shadow-md"> onSubmit={submit}
<IconUserAdd size={16} /> onSubmitFail={(errs) => {
</Avatar> const first = Object.values(errs)[0];
<div> if (first) showError(Array.isArray(first) ? first[0] : first);
<Text className="text-lg font-medium">{t('用户信息')}</Text> formApiRef.current?.scrollToError();
<div className="text-xs text-gray-600">{t('创建新用户账户')}</div> }}
</div> >
</div> <div className="p-6 space-y-6">
<Card className="!rounded-2xl shadow-sm border-0">
<div className="space-y-4"> <div className="flex items-center mb-2">
<div> <Avatar size="small" color="blue" className="mr-2 shadow-md">
<Text strong className="block mb-2">{t('用户名')}</Text> <IconUserAdd size={16} />
<Input </Avatar>
placeholder={t('请输入用户名')} <div>
onChange={(value) => handleInputChange('username', value)} <Text className="text-lg font-medium">{t('用户信息')}</Text>
value={username} <div className="text-xs text-gray-600">{t('创建新用户账户')}</div>
autoComplete="off" </div>
className="!rounded-lg"
showClear
required
/>
</div> </div>
<div> <Row gutter={12}>
<Text strong className="block mb-2">{t('显示名称')}</Text> <Col span={24}>
<Input <Form.Input
placeholder={t('请输入显示名称')} field='username'
onChange={(value) => handleInputChange('display_name', value)} label={t('用户名')}
value={display_name} placeholder={t('请输入用户名')}
autoComplete="off" rules={[{ required: true, message: t('请输入用户名') }]} />
className="!rounded-lg" </Col>
showClear <Col span={24}>
/> <Form.Input
</div> field='display_name'
label={t('显示名称')}
<div> placeholder={t('请输入显示名称')} />
<Text strong className="block mb-2">{t('密码')}</Text> </Col>
<Input <Col span={24}>
type="password" <Form.Input
placeholder={t('请输入密码')} field='password'
onChange={(value) => handleInputChange('password', value)} label={t('密码')}
value={password} type='password'
autoComplete="off" placeholder={t('请输入密码')}
className="!rounded-lg" rules={[{ required: true, message: t('请输入密码') }]} />
required </Col>
/> <Col span={24}>
</div> <Form.Input
field='remark'
<div> label={t('备注')}
<Text strong className="block mb-2">{t('备注')}</Text> placeholder={t('请输入备注(仅管理员可见)')} />
<Input </Col>
placeholder={t('请输入备注(仅管理员可见)')} </Row>
onChange={(value) => handleInputChange('remark', value)} </Card>
value={remark} </div>
autoComplete="off" </Form>
className="!rounded-lg"
showClear
/>
</div>
</div>
</Card>
</div>
</Spin> </Spin>
</SideSheet> </SideSheet>
</> </>

View File

@@ -1,18 +1,27 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next';
import { API, isMobile, showError, showSuccess, renderQuota, renderQuotaWithPrompt } from '../../helpers'; 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 [addQuotaLocal, setAddQuotaLocal] = useState('0');
const [inputs, setInputs] = useState({ 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) => ({ res.data.data.map((g) => ({ label: g, value: g }))
label: group,
value: group,
})),
); );
} catch (error) { } catch (e) {
showError(error.message); showError(e.message);
} }
}; };
const navigate = useNavigate();
const handleCancel = () => { const handleCancel = () => props.handleClose();
props.handleClose();
};
const loadUser = async () => { const loadUser = async () => {
setLoading(true); setLoading(true);
let res = undefined; const url = userId ? `/api/user/${userId}` : `/api/user/self`;
if (userId) { const res = await API.get(url);
res = await API.get(`/api/user/${userId}`);
} else {
res = await API.get(`/api/user/self`);
}
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(); loadUser();
if (userId) { if (userId) fetchGroups();
fetchGroups().then();
}
}, [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) }; payload.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);
} }
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); const current = parseInt(formApiRef.current?.getValue('quota') || 0);
setInputs((inputs) => ({ ...inputs, quota: newQuota })); const delta = parseInt(addQuotaLocal) || 0;
formApiRef.current?.setValue('quota', current + delta);
}; };
const openAddQuotaModal = () => { /* --------------------------- UI --------------------------- */
setAddQuotaLocal('0');
setIsModalOpen(true);
};
const { t } = useTranslation();
return ( return (
<> <>
<SideSheet <SideSheet
placement={'right'} placement='right'
title={ title={
<Space> <Space>
<Tag color="blue" shape="circle">{t('编辑')}</Tag> <Tag color='blue' shape='circle'>
<Title heading={4} className="m-0"> {t(isEdit ? '编辑' : '新建')}
{t('编辑用户')} </Tag>
<Title heading={4} className='m-0'>
{isEdit ? t('编辑用户') : t('创建用户')}
</Title> </Title>
</Space> </Space>
} }
headerStyle={{ headerStyle={{ borderBottom: '1px solid var(--semi-color-border)', padding: '24px' }}
borderBottom: '1px solid var(--semi-color-border)', bodyStyle={{ padding: 0 }}
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" theme='solid'
className="!rounded-full" className='!rounded-full'
onClick={submit} onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />} icon={<IconSave />}
loading={loading} loading={loading}
> >
{t('提交')} {t('提交')}
</Button> </Button>
<Button <Button
theme="light" theme='light'
className="!rounded-full" className='!rounded-full'
type="primary" 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"> <Form
<Card className="!rounded-2xl shadow-sm border-0 mb-6"> initValues={getInitValues()}
{/* Header: Basic Info */} getFormApi={(api) => (formApiRef.current = api)}
<div className="flex items-center mb-2"> onSubmit={submit}
<Avatar size="small" color="blue" className="mr-2 shadow-md"> >
<IconUser size={16} /> {({ values }) => (
</Avatar> <div className='p-6 space-y-6'>
<div> {/* 基本信息 */}
<Text className="text-lg font-medium">{t('基本信息')}</Text> <Card className='!rounded-2xl shadow-sm border-0'>
<div className="text-xs text-gray-600">{t('用户的基本账户信息')}</div> <div className='flex items-center mb-2'>
</div> <Avatar size='small' color='blue' className='mr-2 shadow-md'>
</div> <IconUser size={16} />
</Avatar>
<div className="space-y-4"> <div>
<div> <Text className='text-lg font-medium'>{t('基本信息')}</Text>
<Text strong className="block mb-2">{t('用户')}</Text> <div className='text-xs text-gray-600'>{t('用户的基本账户信息')}</div>
<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>
</div> </div>
<div className="flex gap-2"> </div>
<Input
placeholder={t('请输入新的剩余额度')} <Row gutter={12}>
onChange={(value) => handleInputChange('quota', value)} <Col span={24}>
value={quota} <Form.Input
type="number" field='username'
autoComplete="new-password" label={t('用户名')}
className="flex-1 !rounded-lg" placeholder={t('请输入新的用户名')}
rules={[{ required: true, message: t('请输入用户名') }]}
showClear
/> />
<Button </Col>
onClick={openAddQuotaModal}
className="!rounded-lg" <Col span={24}>
icon={<IconPlus />} <Form.Input
> field='password'
{t('添加额度')} label={t('密码')}
</Button> 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> </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>
)} )}
</Form>
<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>
</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"> <div className='flex items-center'>
<IconPlus className="mr-2" /> <IconPlus className='mr-2' />
{t('添加额度')} {t('添加额度')}
</div> </div>
} }
> >
<div className="mb-4"> <div className='mb-4'>
<Text type="secondary" className="block mb-2"> {
{`${t('新额度')}${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal || 0))}`} (() => {
</Text> 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) => { type='number'
setAddQuotaLocal(value);
}}
value={addQuotaLocal} value={addQuotaLocal}
type="number" onChange={setAddQuotaLocal}
autoComplete="new-password" showClear
className="!rounded-lg"
/> />
</Modal> </Modal>
</> </>