Commit 39fc3866 by t0ng7u

💄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.
parent 0cad369f
...@@ -967,7 +967,7 @@ const PersonalSetting = () => { ...@@ -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"
......
...@@ -471,10 +471,11 @@ ...@@ -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",
"此项只读,要用户通过个人设置页面的相关绑��按钮进��绑���,不可直接修改": "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",
......
...@@ -130,7 +130,7 @@ const Home = () => { ...@@ -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'}
......
...@@ -931,7 +931,7 @@ const TopUp = () => { ...@@ -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
......
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; ...@@ -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) => { ...@@ -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) => { ...@@ -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 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>
</div>
<div className="space-y-4"> <Row gutter={12}>
<div> <Col span={24}>
<Text strong className="block mb-2">{t('用户名')}</Text> <Form.Input
<Input field='username'
placeholder={t('请输入用户名')} label={t('用户名')}
onChange={(value) => handleInputChange('username', value)} placeholder={t('请输入用户名')}
value={username} rules={[{ required: true, message: t('请输入用户名') }]} />
autoComplete="off" </Col>
className="!rounded-lg" <Col span={24}>
showClear <Form.Input
required field='display_name'
/> label={t('显示名称')}
</div> placeholder={t('请输入显示名称')} />
</Col>
<div> <Col span={24}>
<Text strong className="block mb-2">{t('显示名称')}</Text> <Form.Input
<Input field='password'
placeholder={t('请输入显示名称')} label={t('密码')}
onChange={(value) => handleInputChange('display_name', value)} type='password'
value={display_name} placeholder={t('请输入密码')}
autoComplete="off" rules={[{ required: true, message: t('请输入密码') }]} />
className="!rounded-lg" </Col>
showClear <Col span={24}>
/> <Form.Input
</div> field='remark'
label={t('备注')}
<div> placeholder={t('请输入备注(仅管理员可见)')} />
<Text strong className="block mb-2">{t('密码')}</Text> </Col>
<Input </Row>
type="password" </Card>
placeholder={t('请输入密码')} </div>
onChange={(value) => handleInputChange('password', value)} </Form>
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>
</Spin> </Spin>
</SideSheet> </SideSheet>
</> </>
......
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 { ...@@ -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) => { ...@@ -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) => { ...@@ -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) => { ...@@ -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 </div>
placeholder={t('请输入新的用户名')} </div>
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> <Row gutter={12}>
<Text strong className="block mb-2">{t('备注')}</Text> <Col span={24}>
<Input <Form.Input
placeholder={t('请输入备注(仅管理员可见)')} field='username'
onChange={(value) => handleInputChange('remark', value)} label={t('用户名')}
value={remark} placeholder={t('请输入新的用户名')}
autoComplete="off" rules={[{ required: true, message: t('请输入用户名') }]}
className="!rounded-lg" showClear
showClear />
/> </Col>
</div>
</div>
</Card>
{userId && ( <Col span={24}>
<Card className="!rounded-2xl shadow-sm border-0 mb-6"> <Form.Input
{/* Header: Permission Settings */} field='password'
<div className="flex items-center mb-2"> label={t('密码')}
<Avatar size="small" color="green" className="mr-2 shadow-md"> placeholder={t('请输入新的密码,最短 8 位')}
<IconUserGroup size={16} /> mode='password'
</Avatar> showClear
<div> />
<Text className="text-lg font-medium">{t('权限设置')}</Text> </Col>
<div className="text-xs text-gray-600">{t('用户分组和额度管理')}</div>
</div>
</div>
<div className="space-y-4"> <Col span={24}>
<div> <Form.Input
<Text strong className="block mb-2">{t('分组')}</Text> field='display_name'
<Select label={t('显示名称')}
placeholder={t('请选择分组')} placeholder={t('请输入新的显示名称')}
search showClear
allowAdditions />
additionLabel={t( </Col>
'请在系统设置页面编辑分组倍率以添加新的分组:',
)}
onChange={(value) => handleInputChange('group', value)}
value={inputs.group}
autoComplete="new-password"
optionList={groupOptions}
className="w-full !rounded-lg"
/>
</div>
<div> <Col span={24}>
<div className="flex justify-between mb-2"> <Form.Input
<Text strong>{t('剩余额度')}</Text> field='remark'
<Text type="tertiary">{renderQuotaWithPrompt(quota)}</Text> label={t('备注')}
</div> placeholder={t('请输入备注(仅管理员可见)')}
<div className="flex gap-2"> showClear
<Input
placeholder={t('请输入新的剩余额度')}
onChange={(value) => handleInputChange('quota', value)}
value={quota}
type="number"
autoComplete="new-password"
className="flex-1 !rounded-lg"
/> />
<Button </Col>
onClick={openAddQuotaModal} </Row>
className="!rounded-lg" </Card>
icon={<IconPlus />}
>
{t('添加额度')}
</Button>
</div>
</div>
</div>
</Card>
)}
<Card className="!rounded-2xl shadow-sm border-0"> {/* 权限设置 */}
{/* Header: Bindings */} {userId && (
<div className="flex items-center mb-2"> <Card className='!rounded-2xl shadow-sm border-0'>
<Avatar size="small" color="purple" className="mr-2 shadow-md"> <div className='flex items-center mb-2'>
<IconLink size={16} /> <Avatar size='small' color='green' className='mr-2 shadow-md'>
</Avatar> <IconUserGroup size={16} />
<div> </Avatar>
<Text className="text-lg font-medium">{t('绑定信息')}</Text> <div>
<div className="text-xs text-gray-600">{t('第三方账户绑定状态(只读)')}</div> <Text className='text-lg font-medium'>{t('权限设置')}</Text>
</div> <div className='text-xs text-gray-600'>{t('用户分组和额度管理')}</div>
</div> </div>
</div>
<div className="space-y-4"> <Row gutter={12}>
<div> <Col span={24}>
<Text strong className="block mb-2">{t('已绑定的 GitHub 账户')}</Text> <Form.Select
<Input field='group'
value={github_id} label={t('分组')}
autoComplete="new-password" placeholder={t('请选择分组')}
placeholder={t( optionList={groupOptions}
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改', allowAdditions
)} search
readonly rules={[{ required: true, message: t('请选择分组') }]}
className="!rounded-lg" />
/> </Col>
</div>
<div> <Col span={10}>
<Text strong className="block mb-2">{t('已绑定的 OIDC 账户')}</Text> <Form.InputNumber
<Input field='quota'
value={oidc_id} label={t('剩余额度')}
placeholder={t( placeholder={t('请输入新的剩余额度')}
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改', min={0}
)} step={500000}
readonly extraText={renderQuotaWithPrompt(values.quota || 0)}
className="!rounded-lg" rules={[{ required: true, message: t('请输入额度') }]}
/> style={{ width: '100%' }}
</div> />
</Col>
<div> <Col span={14}>
<Text strong className="block mb-2">{t('已绑定的微信账户')}</Text> <Form.Slot label={t('添加额度')}>
<Input <Button
value={wechat_id} icon={<IconPlus />}
autoComplete="new-password" onClick={() => setIsModalOpen(true)}
placeholder={t( />
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改', </Form.Slot>
)} </Col>
readonly </Row>
className="!rounded-lg" </Card>
/> )}
</div>
<div> {/* 绑定信息 */}
<Text strong className="block mb-2">{t('已绑定的邮箱账户')}</Text> <Card className='!rounded-2xl shadow-sm border-0'>
<Input <div className='flex items-center mb-2'>
value={email} <Avatar size='small' color='purple' className='mr-2 shadow-md'>
autoComplete="new-password" <IconLink size={16} />
placeholder={t( </Avatar>
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改', <div>
)} <Text className='text-lg font-medium'>{t('绑定信息')}</Text>
readonly <div className='text-xs text-gray-600'>{t('第三方账户绑定状态(只读)')}</div>
className="!rounded-lg" </div>
/> </div>
</div>
<div> <Row gutter={12}>
<Text strong className="block mb-2">{t('已绑定的 Telegram 账户')}</Text> {['github_id', 'oidc_id', 'wechat_id', 'email', 'telegram_id'].map((field) => (
<Input <Col span={24} key={field}>
value={telegram_id} <Form.Input
autoComplete="new-password" field={field}
placeholder={t( label={t(`已绑定的 ${field.replace('_id', '').toUpperCase()} 账户`)}
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改', readonly
)} placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly />
className="!rounded-lg" </Col>
/> ))}
</div> </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) => { ...@@ -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>
</> </>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment