- Added new translation keys for English localization in `en.json`, including "Token group, default is the your's group" and "IP whitelist (do not overly trust this function)". - Refactored `EditToken.js` to utilize the `useTranslation` hook for improved internationalization, ensuring all user-facing strings are translatable. - Updated error and success messages to use translation functions, enhancing user experience for non-English speakers. - Improved UI elements to support localization, including labels, placeholders, and button texts, ensuring consistency across the token editing interface.
470 lines
14 KiB
JavaScript
470 lines
14 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import {
|
||
API,
|
||
isMobile,
|
||
showError,
|
||
showSuccess,
|
||
timestamp2string,
|
||
} from '../../helpers';
|
||
import { renderQuotaWithPrompt } from '../../helpers/render';
|
||
import {
|
||
AutoComplete,
|
||
Banner,
|
||
Button,
|
||
Checkbox,
|
||
DatePicker,
|
||
Input,
|
||
Select,
|
||
SideSheet,
|
||
Space,
|
||
Spin, TextArea,
|
||
Typography
|
||
} from '@douyinfe/semi-ui';
|
||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||
import { Divider } from 'semantic-ui-react';
|
||
import { useTranslation } from 'react-i18next';
|
||
|
||
const EditToken = (props) => {
|
||
const [isEdit, setIsEdit] = useState(false);
|
||
const [loading, setLoading] = useState(isEdit);
|
||
const originInputs = {
|
||
name: '',
|
||
remain_quota: isEdit ? 0 : 500000,
|
||
expired_time: -1,
|
||
unlimited_quota: false,
|
||
model_limits_enabled: false,
|
||
model_limits: [],
|
||
allow_ips: '',
|
||
group: '',
|
||
};
|
||
const [inputs, setInputs] = useState(originInputs);
|
||
const {
|
||
name,
|
||
remain_quota,
|
||
expired_time,
|
||
unlimited_quota,
|
||
model_limits_enabled,
|
||
model_limits,
|
||
allow_ips,
|
||
group
|
||
} = inputs;
|
||
// const [visible, setVisible] = useState(false);
|
||
const [models, setModels] = useState([]);
|
||
const [groups, setGroups] = useState([]);
|
||
const navigate = useNavigate();
|
||
const { t } = useTranslation();
|
||
const handleInputChange = (name, value) => {
|
||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||
};
|
||
const handleCancel = () => {
|
||
props.handleClose();
|
||
};
|
||
const setExpiredTime = (month, day, hour, minute) => {
|
||
let now = new Date();
|
||
let timestamp = now.getTime() / 1000;
|
||
let seconds = month * 30 * 24 * 60 * 60;
|
||
seconds += day * 24 * 60 * 60;
|
||
seconds += hour * 60 * 60;
|
||
seconds += minute * 60;
|
||
if (seconds !== 0) {
|
||
timestamp += seconds;
|
||
setInputs({ ...inputs, expired_time: timestamp2string(timestamp) });
|
||
} else {
|
||
setInputs({ ...inputs, expired_time: -1 });
|
||
}
|
||
};
|
||
|
||
const setUnlimitedQuota = () => {
|
||
setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
|
||
};
|
||
|
||
const loadModels = async () => {
|
||
let res = await API.get(`/api/user/models`);
|
||
const { success, message, data } = res.data;
|
||
if (success) {
|
||
let localModelOptions = data.map((model) => ({
|
||
label: model,
|
||
value: model,
|
||
}));
|
||
setModels(localModelOptions);
|
||
} else {
|
||
showError(t(message));
|
||
}
|
||
};
|
||
|
||
const loadGroups = async () => {
|
||
let res = await API.get(`/api/user/self/groups`);
|
||
const { success, message, data } = res.data;
|
||
if (success) {
|
||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||
label: data[group],
|
||
value: group,
|
||
}));
|
||
setGroups(localGroupOptions);
|
||
} else {
|
||
showError(t(message));
|
||
}
|
||
};
|
||
|
||
const loadToken = async () => {
|
||
setLoading(true);
|
||
let res = await API.get(`/api/token/${props.editingToken.id}`);
|
||
const { success, message, data } = res.data;
|
||
if (success) {
|
||
if (data.expired_time !== -1) {
|
||
data.expired_time = timestamp2string(data.expired_time);
|
||
}
|
||
if (data.model_limits !== '') {
|
||
data.model_limits = data.model_limits.split(',');
|
||
} else {
|
||
data.model_limits = [];
|
||
}
|
||
setInputs(data);
|
||
} else {
|
||
showError(message);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
useEffect(() => {
|
||
setIsEdit(props.editingToken.id !== undefined);
|
||
}, [props.editingToken.id]);
|
||
|
||
useEffect(() => {
|
||
if (!isEdit) {
|
||
setInputs(originInputs);
|
||
} else {
|
||
loadToken().then(() => {
|
||
// console.log(inputs);
|
||
});
|
||
}
|
||
loadModels();
|
||
loadGroups();
|
||
}, [isEdit]);
|
||
|
||
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
|
||
const [tokenCount, setTokenCount] = useState(1);
|
||
|
||
// 新增处理 tokenCount 变化的函数
|
||
const handleTokenCountChange = (value) => {
|
||
// 确保用户输入的是正整数
|
||
const count = parseInt(value, 10);
|
||
if (!isNaN(count) && count > 0) {
|
||
setTokenCount(count);
|
||
}
|
||
};
|
||
|
||
// 生成一个随机的四位字母数字字符串
|
||
const generateRandomSuffix = () => {
|
||
const characters =
|
||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||
let result = '';
|
||
for (let i = 0; i < 6; i++) {
|
||
result += characters.charAt(
|
||
Math.floor(Math.random() * characters.length),
|
||
);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
const submit = async () => {
|
||
setLoading(true);
|
||
if (isEdit) {
|
||
// 编辑令牌的逻辑保持不变
|
||
let localInputs = { ...inputs };
|
||
localInputs.remain_quota = parseInt(localInputs.remain_quota);
|
||
if (localInputs.expired_time !== -1) {
|
||
let time = Date.parse(localInputs.expired_time);
|
||
if (isNaN(time)) {
|
||
showError(t('过期时间格式错误!'));
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
localInputs.expired_time = Math.ceil(time / 1000);
|
||
}
|
||
localInputs.model_limits = localInputs.model_limits.join(',');
|
||
let res = await API.put(`/api/token/`, {
|
||
...localInputs,
|
||
id: parseInt(props.editingToken.id),
|
||
});
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('令牌更新成功!'));
|
||
props.refresh();
|
||
props.handleClose();
|
||
} else {
|
||
showError(t(message));
|
||
}
|
||
} else {
|
||
// 处理新增多个令牌的情况
|
||
let successCount = 0; // 记录成功创建的令牌数量
|
||
for (let i = 0; i < tokenCount; i++) {
|
||
let localInputs = { ...inputs };
|
||
if (i !== 0) {
|
||
// 如果用户想要创建多个令牌,则给每个令牌一个序号后缀
|
||
localInputs.name = `${inputs.name}-${generateRandomSuffix()}`;
|
||
}
|
||
localInputs.remain_quota = parseInt(localInputs.remain_quota);
|
||
|
||
if (localInputs.expired_time !== -1) {
|
||
let time = Date.parse(localInputs.expired_time);
|
||
if (isNaN(time)) {
|
||
showError(t('过期时间格式错误!'));
|
||
setLoading(false);
|
||
break;
|
||
}
|
||
localInputs.expired_time = Math.ceil(time / 1000);
|
||
}
|
||
localInputs.model_limits = localInputs.model_limits.join(',');
|
||
let res = await API.post(`/api/token/`, localInputs);
|
||
const { success, message } = res.data;
|
||
|
||
if (success) {
|
||
successCount++;
|
||
} else {
|
||
showError(t(message));
|
||
break; // 如果创建失败,终止循环
|
||
}
|
||
}
|
||
|
||
if (successCount > 0) {
|
||
showSuccess(
|
||
t('令牌创建成功,请在列表页面点击复制获取令牌!')
|
||
);
|
||
props.refresh();
|
||
props.handleClose();
|
||
}
|
||
}
|
||
setLoading(false);
|
||
setInputs(originInputs); // 重置表单
|
||
setTokenCount(1); // 重置数量为默认值
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<SideSheet
|
||
placement={isEdit ? 'right' : 'left'}
|
||
title={
|
||
<Title level={3}>{isEdit ? t('更新令牌信息') : t('创建新的令牌')}</Title>
|
||
}
|
||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||
visible={props.visiable}
|
||
footer={
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||
<Space>
|
||
<Button theme='solid' size={'large'} onClick={submit}>
|
||
{t('提交')}
|
||
</Button>
|
||
<Button
|
||
theme='solid'
|
||
size={'large'}
|
||
type={'tertiary'}
|
||
onClick={handleCancel}
|
||
>
|
||
{t('取消')}
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
}
|
||
closeIcon={null}
|
||
onCancel={() => handleCancel()}
|
||
width={isMobile() ? '100%' : 600}
|
||
>
|
||
<Spin spinning={loading}>
|
||
<Input
|
||
style={{ marginTop: 20 }}
|
||
label={t('名称')}
|
||
name='name'
|
||
placeholder={t('请输入名称')}
|
||
onChange={(value) => handleInputChange('name', value)}
|
||
value={name}
|
||
autoComplete='new-password'
|
||
required={!isEdit}
|
||
/>
|
||
<Divider />
|
||
<DatePicker
|
||
label={t('过期时间')}
|
||
name='expired_time'
|
||
placeholder={t('请选择过期时间')}
|
||
onChange={(value) => handleInputChange('expired_time', value)}
|
||
value={expired_time}
|
||
autoComplete='new-password'
|
||
type='dateTime'
|
||
/>
|
||
<div style={{ marginTop: 20 }}>
|
||
<Space>
|
||
<Button
|
||
type={'tertiary'}
|
||
onClick={() => {
|
||
setExpiredTime(0, 0, 0, 0);
|
||
}}
|
||
>
|
||
{t('永不过期')}
|
||
</Button>
|
||
<Button
|
||
type={'tertiary'}
|
||
onClick={() => {
|
||
setExpiredTime(0, 0, 1, 0);
|
||
}}
|
||
>
|
||
{t('一小时')}
|
||
</Button>
|
||
<Button
|
||
type={'tertiary'}
|
||
onClick={() => {
|
||
setExpiredTime(1, 0, 0, 0);
|
||
}}
|
||
>
|
||
{t('一个月')}
|
||
</Button>
|
||
<Button
|
||
type={'tertiary'}
|
||
onClick={() => {
|
||
setExpiredTime(0, 1, 0, 0);
|
||
}}
|
||
>
|
||
{t('一天')}
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
|
||
<Divider />
|
||
<Banner
|
||
type={'warning'}
|
||
description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
|
||
></Banner>
|
||
<div style={{ marginTop: 20 }}>
|
||
<Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
|
||
</div>
|
||
<AutoComplete
|
||
style={{ marginTop: 8 }}
|
||
name='remain_quota'
|
||
placeholder={t('请输入额度')}
|
||
onChange={(value) => handleInputChange('remain_quota', value)}
|
||
value={remain_quota}
|
||
autoComplete='new-password'
|
||
type='number'
|
||
// position={'top'}
|
||
data={[
|
||
{ value: 500000, label: '1$' },
|
||
{ value: 5000000, label: '10$' },
|
||
{ value: 25000000, label: '50$' },
|
||
{ value: 50000000, label: '100$' },
|
||
{ value: 250000000, label: '500$' },
|
||
{ value: 500000000, label: '1000$' },
|
||
]}
|
||
disabled={unlimited_quota}
|
||
/>
|
||
|
||
{!isEdit && (
|
||
<>
|
||
<div style={{ marginTop: 20 }}>
|
||
<Typography.Text>{t('新建数量')}</Typography.Text>
|
||
</div>
|
||
<AutoComplete
|
||
style={{ marginTop: 8 }}
|
||
label={t('数量')}
|
||
placeholder={t('请选择或输入创建令牌的数量')}
|
||
onChange={(value) => handleTokenCountChange(value)}
|
||
onSelect={(value) => handleTokenCountChange(value)}
|
||
value={tokenCount.toString()}
|
||
autoComplete='off'
|
||
type='number'
|
||
data={[
|
||
{ value: 10, label: t('10个') },
|
||
{ value: 20, label: t('20个') },
|
||
{ value: 30, label: t('30个') },
|
||
{ value: 100, label: t('100个') },
|
||
]}
|
||
disabled={unlimited_quota}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
<div>
|
||
<Button
|
||
style={{ marginTop: 8 }}
|
||
type={'warning'}
|
||
onClick={() => {
|
||
setUnlimitedQuota();
|
||
}}
|
||
>
|
||
{unlimited_quota ? t('取消<E58F96><E6B688><EFBFBD>限额度') : t('设为无限额度')}
|
||
</Button>
|
||
</div>
|
||
<Divider />
|
||
<div style={{ marginTop: 10 }}>
|
||
<Typography.Text>{t('IP白名单(请勿过度信任此功能)')}</Typography.Text>
|
||
</div>
|
||
<TextArea
|
||
label={t('IP白名单')}
|
||
name='allow_ips'
|
||
placeholder={t('允许的IP,一行一个')}
|
||
onChange={(value) => {
|
||
handleInputChange('allow_ips', value);
|
||
}}
|
||
value={inputs.allow_ips}
|
||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||
/>
|
||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||
<Space>
|
||
<Checkbox
|
||
name='model_limits_enabled'
|
||
checked={model_limits_enabled}
|
||
onChange={(e) =>
|
||
handleInputChange('model_limits_enabled', e.target.checked)
|
||
}
|
||
>
|
||
{t('启用模型限制(非必要,不建议启用)')}
|
||
</Checkbox>
|
||
</Space>
|
||
</div>
|
||
|
||
<Select
|
||
style={{ marginTop: 8 }}
|
||
placeholder={t('请选择该渠道所支持的模型')}
|
||
name='models'
|
||
required
|
||
multiple
|
||
selection
|
||
onChange={(value) => {
|
||
handleInputChange('model_limits', value);
|
||
}}
|
||
value={inputs.model_limits}
|
||
autoComplete='new-password'
|
||
optionList={models}
|
||
disabled={!model_limits_enabled}
|
||
/>
|
||
<div style={{ marginTop: 10 }}>
|
||
<Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
|
||
</div>
|
||
{groups.length > 0 ?
|
||
<Select
|
||
style={{ marginTop: 8 }}
|
||
placeholder={t('令牌分组,默认为用户的分组')}
|
||
name='gruop'
|
||
required
|
||
selection
|
||
onChange={(value) => {
|
||
handleInputChange('group', value);
|
||
}}
|
||
value={inputs.group}
|
||
autoComplete='new-password'
|
||
optionList={groups}
|
||
/>:
|
||
<Select
|
||
style={{ marginTop: 8 }}
|
||
placeholder={t('管理员未设置用户可选分组')}
|
||
name='gruop'
|
||
disabled={true}
|
||
/>
|
||
}
|
||
</Spin>
|
||
</SideSheet>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default EditToken;
|