new-api/web/src/pages/Token/EditToken.js

606 lines
19 KiB
JavaScript
Raw Normal View History

2025-06-16 22:15:12 +08:00
import React, { useEffect, useState, useContext } from 'react';
2024-03-15 16:05:33 +08:00
import { useNavigate } from 'react-router-dom';
2024-03-23 21:24:39 +08:00
import {
API,
isMobile,
showError,
showSuccess,
timestamp2string,
renderGroupOption,
2025-06-16 22:15:12 +08:00
renderQuotaWithPrompt,
2024-03-23 21:24:39 +08:00
} from '../../helpers';
import {
2024-03-23 21:24:39 +08:00
AutoComplete,
Banner,
Button,
Checkbox,
DatePicker,
Input,
Select,
SideSheet,
Space,
2025-04-04 12:00:38 +08:00
Spin,
TextArea,
Typography,
2025-05-23 00:24:08 +08:00
Card,
Tag,
Avatar,
2024-03-15 16:05:33 +08:00
} from '@douyinfe/semi-ui';
2025-05-23 00:24:08 +08:00
import {
IconCreditCard,
IconLink,
IconUserGroup,
IconSave,
IconClose,
IconPlusCircle,
} from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next';
2025-06-16 22:15:12 +08:00
import { StatusContext } from '../../context/Status';
2023-04-23 12:43:10 +08:00
2025-05-23 00:24:08 +08:00
const { Text, Title } = Typography;
2023-11-01 02:50:14 +08:00
const EditToken = (props) => {
2025-05-23 00:24:08 +08:00
const { t } = useTranslation();
2025-06-16 22:15:12 +08:00
const [statusState, statusDispatch] = useContext(StatusContext);
2024-03-15 16:05:33 +08:00
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,
2024-03-23 21:24:39 +08:00
model_limits: [],
2024-09-17 20:49:51 +08:00
allow_ips: '',
2024-09-18 05:19:10 +08:00
group: '',
2024-03-15 16:05:33 +08:00
};
const [inputs, setInputs] = useState(originInputs);
2024-03-23 21:24:39 +08:00
const {
name,
remain_quota,
expired_time,
unlimited_quota,
model_limits_enabled,
model_limits,
2024-09-18 05:19:10 +08:00
allow_ips,
2025-04-04 12:00:38 +08:00
group,
2024-03-23 21:24:39 +08:00
} = inputs;
2024-10-12 19:36:55 +08:00
const [models, setModels] = useState([]);
2024-09-18 05:19:10 +08:00
const [groups, setGroups] = useState([]);
2024-03-15 16:05:33 +08:00
const navigate = useNavigate();
2025-05-23 00:24:08 +08:00
2024-03-15 16:05:33 +08:00
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
2025-05-23 00:24:08 +08:00
2024-03-15 16:05:33 +08:00
const handleCancel = () => {
props.handleClose();
};
2025-05-23 00:24:08 +08:00
2024-03-15 16:05:33 +08:00
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 });
}
2024-03-15 16:05:33 +08:00
};
2024-03-15 16:05:33 +08:00
const setUnlimitedQuota = () => {
setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
};
2024-03-15 16:05:33 +08:00
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,
2024-03-23 21:24:39 +08:00
value: model,
2024-03-15 16:05:33 +08:00
}));
setModels(localModelOptions);
} else {
showError(t(message));
}
2024-03-15 16:05:33 +08:00
};
2024-09-18 05:19:10 +08:00
const loadGroups = async () => {
let res = await API.get(`/api/user/self/groups`);
2024-09-18 05:19:10 +08:00
const { success, message, data } = res.data;
if (success) {
let localGroupOptions = Object.entries(data).map(([group, info]) => ({
label: info.desc,
value: group,
2025-04-04 12:00:38 +08:00
ratio: info.ratio,
}));
2025-06-16 22:15:12 +08:00
if (statusState?.status?.default_use_auto_group) {
// if contain auto, add it to the first position
if (localGroupOptions.some((group) => group.value === 'auto')) {
// 排序
localGroupOptions.sort((a, b) => (a.value === 'auto' ? -1 : 1));
} else {
localGroupOptions.unshift({ label: t('自动选择'), value: 'auto' });
}
}
setGroups(localGroupOptions);
2025-06-16 22:15:12 +08:00
if (statusState?.status?.default_use_auto_group) {
setInputs({ ...inputs, group: 'auto' });
}
2024-09-18 05:19:10 +08:00
} else {
showError(t(message));
2024-09-18 05:19:10 +08:00
}
};
2024-03-15 16:05:33 +08:00
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);
};
2025-05-23 00:24:08 +08:00
2024-03-15 16:05:33 +08:00
useEffect(() => {
setIsEdit(props.editingToken.id !== undefined);
}, [props.editingToken.id]);
useEffect(() => {
if (!isEdit) {
setInputs(originInputs);
} else {
2024-03-23 21:24:39 +08:00
loadToken().then(() => {
// console.log(inputs);
});
2024-03-15 16:05:33 +08:00
}
loadModels();
2024-09-18 05:19:10 +08:00
loadGroups();
2024-03-15 16:05:33 +08:00
}, [isEdit]);
2023-04-23 12:43:10 +08:00
2024-03-15 16:05:33 +08:00
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
const [tokenCount, setTokenCount] = useState(1);
2024-03-15 16:05:33 +08:00
// 新增处理 tokenCount 变化的函数
const handleTokenCountChange = (value) => {
// 确保用户输入的是正整数
const count = parseInt(value, 10);
if (!isNaN(count) && count > 0) {
setTokenCount(count);
}
};
// 生成一个随机的四位字母数字字符串
const generateRandomSuffix = () => {
2024-03-23 21:24:39 +08:00
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
2024-03-15 16:05:33 +08:00
let result = '';
for (let i = 0; i < 6; i++) {
2024-03-23 21:24:39 +08:00
result += characters.charAt(
Math.floor(Math.random() * characters.length),
);
2024-03-15 16:05:33 +08:00
}
return result;
};
2023-11-19 14:06:08 +08:00
2024-03-15 16:05:33 +08:00
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('过期时间格式错误!'));
2024-03-15 16:05:33 +08:00
setLoading(false);
return;
}
localInputs.expired_time = Math.ceil(time / 1000);
}
localInputs.model_limits = localInputs.model_limits.join(',');
2024-03-23 21:24:39 +08:00
let res = await API.put(`/api/token/`, {
...localInputs,
id: parseInt(props.editingToken.id),
});
2024-03-15 16:05:33 +08:00
const { success, message } = res.data;
if (success) {
showSuccess(t('令牌更新成功!'));
2024-03-15 16:05:33 +08:00
props.refresh();
props.handleClose();
} else {
showError(t(message));
2024-03-15 16:05:33 +08:00
}
} else {
// 处理新增多个令牌的情况
let successCount = 0; // 记录成功创建的令牌数量
for (let i = 0; i < tokenCount; i++) {
let localInputs = { ...inputs };
// 检查用户是否填写了令牌名称
const baseName = inputs.name.trim() === '' ? 'default' : inputs.name;
if (i !== 0 || inputs.name.trim() === '') {
// 如果创建多个令牌i !== 0或者用户没有填写名称则添加随机后缀
localInputs.name = `${baseName}-${generateRandomSuffix()}`;
} else {
localInputs.name = baseName;
2023-11-19 14:06:08 +08:00
}
2024-03-15 16:05:33 +08:00
localInputs.remain_quota = parseInt(localInputs.remain_quota);
2023-11-19 14:06:08 +08:00
2024-03-15 16:05:33 +08:00
if (localInputs.expired_time !== -1) {
let time = Date.parse(localInputs.expired_time);
if (isNaN(time)) {
showError(t('过期时间格式错误!'));
2024-03-15 16:05:33 +08:00
setLoading(false);
break;
}
localInputs.expired_time = Math.ceil(time / 1000);
2023-11-19 14:06:08 +08:00
}
2024-03-15 16:05:33 +08:00
localInputs.model_limits = localInputs.model_limits.join(',');
let res = await API.post(`/api/token/`, localInputs);
const { success, message } = res.data;
2023-11-19 14:06:08 +08:00
2024-03-15 16:05:33 +08:00
if (success) {
successCount++;
2023-11-01 02:50:14 +08:00
} else {
showError(t(message));
2024-03-15 16:05:33 +08:00
break; // 如果创建失败,终止循环
2023-11-01 02:50:14 +08:00
}
2024-03-15 16:05:33 +08:00
}
2023-11-01 02:50:14 +08:00
2024-03-15 16:05:33 +08:00
if (successCount > 0) {
2025-04-04 12:00:38 +08:00
showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!'));
2024-03-15 16:05:33 +08:00
props.refresh();
props.handleClose();
}
}
setLoading(false);
setInputs(originInputs); // 重置表单
setTokenCount(1); // 重置数量为默认值
};
2023-11-01 02:50:14 +08:00
2024-03-15 16:05:33 +08:00
return (
2025-05-23 00:24:08 +08:00
<SideSheet
placement={isEdit ? 'right' : 'left'}
title={
<Space>
2025-06-16 22:15:12 +08:00
{isEdit ? (
<Tag color='blue' shape='circle'>
{t('更新')}
</Tag>
) : (
<Tag color='green' shape='circle'>
{t('新建')}
</Tag>
)}
<Title heading={4} className='m-0'>
2025-04-04 12:00:38 +08:00
{isEdit ? t('更新令牌信息') : t('创建新的令牌')}
</Title>
2025-05-23 00:24:08 +08:00
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
2025-06-16 22:15:12 +08:00
padding: '24px',
2025-05-23 00:24:08 +08:00
}}
bodyStyle={{ padding: '0' }}
2025-05-23 00:24:08 +08:00
visible={props.visiable}
width={isMobile() ? '100%' : 600}
footer={
2025-06-16 22:15:12 +08:00
<div className='flex justify-end bg-white'>
2025-05-23 00:24:08 +08:00
<Space>
2024-03-23 21:24:39 +08:00
<Button
2025-06-16 22:15:12 +08:00
theme='solid'
className='!rounded-full'
2025-05-23 00:24:08 +08:00
onClick={submit}
icon={<IconSave />}
loading={loading}
2024-03-23 21:24:39 +08:00
>
2025-05-23 00:24:08 +08:00
{t('提交')}
2024-03-23 21:24:39 +08:00
</Button>
2025-05-23 00:24:08 +08:00
<Button
2025-06-16 22:15:12 +08:00
theme='light'
className='!rounded-full'
type='primary'
2025-05-23 00:24:08 +08:00
onClick={handleCancel}
icon={<IconClose />}
>
{t('取消')}
</Button>
</Space>
</div>
}
closeIcon={null}
onCancel={() => handleCancel()}
>
<Spin spinning={loading}>
2025-06-16 22:15:12 +08:00
<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'>
<IconPlusCircle size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('基本信息')}</Text>
<div className='text-xs text-gray-600'>{t('设置令牌的基本信息')}</div>
2025-05-23 00:24:08 +08:00
</div>
</div>
2025-06-16 22:15:12 +08:00
<div className='space-y-4'>
2025-05-23 00:24:08 +08:00
<div>
2025-06-16 22:15:12 +08:00
<Text strong className='block mb-2'>
{t('名称')}
</Text>
2025-05-23 00:24:08 +08:00
<Input
placeholder={t('请输入名称')}
onChange={(value) => handleInputChange('name', value)}
value={name}
2025-06-16 22:15:12 +08:00
autoComplete='new-password'
className='!rounded-lg'
2025-05-23 00:24:08 +08:00
showClear
required
/>
</div>
<div>
2025-06-16 22:15:12 +08:00
<Text strong className='block mb-2'>
{t('过期时间')}
</Text>
<div className='mb-2'>
2025-05-23 00:24:08 +08:00
<DatePicker
placeholder={t('请选择过期时间')}
2025-06-16 22:15:12 +08:00
onChange={(value) =>
handleInputChange('expired_time', value)
}
2025-05-23 00:24:08 +08:00
value={expired_time}
2025-06-16 22:15:12 +08:00
autoComplete='new-password'
type='dateTime'
className='w-full !rounded-lg'
2025-05-23 00:24:08 +08:00
/>
</div>
2025-06-16 22:15:12 +08:00
<div className='flex flex-wrap gap-2'>
2025-05-23 00:24:08 +08:00
<Button
2025-06-16 22:15:12 +08:00
theme='light'
type='primary'
2025-05-23 00:24:08 +08:00
onClick={() => setExpiredTime(0, 0, 0, 0)}
2025-06-16 22:15:12 +08:00
className='!rounded-full'
2025-05-23 00:24:08 +08:00
>
{t('永不过期')}
</Button>
<Button
2025-06-16 22:15:12 +08:00
theme='light'
type='tertiary'
2025-05-23 00:24:08 +08:00
onClick={() => setExpiredTime(0, 0, 1, 0)}
2025-06-16 22:15:12 +08:00
className='!rounded-full'
2025-05-23 00:24:08 +08:00
>
{t('一小时')}
</Button>
<Button
2025-06-16 22:15:12 +08:00
theme='light'
type='tertiary'
2025-05-23 00:24:08 +08:00
onClick={() => setExpiredTime(0, 1, 0, 0)}
2025-06-16 22:15:12 +08:00
className='!rounded-full'
2025-05-23 00:24:08 +08:00
>
{t('一天')}
</Button>
<Button
2025-06-16 22:15:12 +08:00
theme='light'
type='tertiary'
2025-05-23 00:24:08 +08:00
onClick={() => setExpiredTime(1, 0, 0, 0)}
2025-06-16 22:15:12 +08:00
className='!rounded-full'
2025-05-23 00:24:08 +08:00
>
{t('一个月')}
</Button>
</div>
</div>
</div>
</Card>
2025-06-16 22:15:12 +08:00
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
{/* Header: Quota Settings */}
<div className='flex items-center mb-2'>
<Avatar size='small' color='green' className='mr-2 shadow-md'>
<IconCreditCard size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('额度设置')}</Text>
<div className='text-xs text-gray-600'>{t('设置令牌可用额度和数量')}</div>
2025-05-23 00:24:08 +08:00
</div>
</div>
<Banner
2025-06-16 22:15:12 +08:00
type='warning'
description={t(
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
)}
className='mb-4 !rounded-lg'
/>
2025-05-23 00:24:08 +08:00
2025-06-16 22:15:12 +08:00
<div className='space-y-4'>
2025-05-23 00:24:08 +08:00
<div>
2025-06-16 22:15:12 +08:00
<div className='flex justify-between mb-2'>
2025-05-23 00:24:08 +08:00
<Text strong>{t('额度')}</Text>
2025-06-16 22:15:12 +08:00
<Text type='tertiary'>
{renderQuotaWithPrompt(remain_quota)}
</Text>
2025-05-23 00:24:08 +08:00
</div>
<AutoComplete
placeholder={t('请输入额度')}
onChange={(value) => handleInputChange('remain_quota', value)}
value={remain_quota}
2025-06-16 22:15:12 +08:00
autoComplete='new-password'
type='number'
className='w-full !rounded-lg'
2025-05-23 00:24:08 +08:00
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}
/>
</div>
{!isEdit && (
<div>
2025-06-16 22:15:12 +08:00
<Text strong className='block mb-2'>
{t('新建数量')}
</Text>
2025-05-23 00:24:08 +08:00
<AutoComplete
placeholder={t('请选择或输入创建令牌的数量')}
onChange={(value) => handleTokenCountChange(value)}
onSelect={(value) => handleTokenCountChange(value)}
value={tokenCount.toString()}
2025-06-16 22:15:12 +08:00
autoComplete='off'
type='number'
className='w-full !rounded-lg'
2025-05-23 00:24:08 +08:00
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>
)}
2025-06-16 22:15:12 +08:00
<div className='flex justify-end'>
2025-05-23 00:24:08 +08:00
<Button
2025-06-16 22:15:12 +08:00
theme='light'
type={unlimited_quota ? 'danger' : 'warning'}
2025-05-23 00:24:08 +08:00
onClick={setUnlimitedQuota}
2025-06-16 22:15:12 +08:00
className='!rounded-full'
2025-05-23 00:24:08 +08:00
>
{unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
</Button>
</div>
</div>
</Card>
2025-06-16 22:15:12 +08:00
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
{/* Header: Access Limits */}
<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>
2025-05-23 00:24:08 +08:00
</div>
</div>
2025-06-16 22:15:12 +08:00
<div className='space-y-4'>
2025-05-23 00:24:08 +08:00
<div>
2025-06-16 22:15:12 +08:00
<Text strong className='block mb-2'>
{t('IP白名单')}
</Text>
2025-05-23 00:24:08 +08:00
<TextArea
placeholder={t('允许的IP一行一个不填写则不限制')}
onChange={(value) => handleInputChange('allow_ips', value)}
value={inputs.allow_ips}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
2025-06-16 22:15:12 +08:00
className='!rounded-lg'
2025-05-23 00:24:08 +08:00
rows={4}
/>
2025-06-16 22:15:12 +08:00
<Text type='tertiary' className='mt-1 block text-xs'>
{t('请勿过度信任此功能IP可能被伪造')}
</Text>
2025-05-23 00:24:08 +08:00
</div>
<div>
2025-06-16 22:15:12 +08:00
<div className='flex items-center mb-2'>
2025-05-23 00:24:08 +08:00
<Checkbox
checked={model_limits_enabled}
2025-06-16 22:15:12 +08:00
onChange={(e) =>
handleInputChange(
'model_limits_enabled',
e.target.checked,
)
}
2025-05-23 00:24:08 +08:00
>
<Text strong>{t('模型限制')}</Text>
</Checkbox>
</div>
<Select
2025-06-16 22:15:12 +08:00
placeholder={
model_limits_enabled
? t('请选择该渠道所支持的模型')
: t('勾选启用模型限制后可选择')
}
2025-05-23 00:24:08 +08:00
onChange={(value) => handleInputChange('model_limits', value)}
value={inputs.model_limits}
multiple
2025-06-16 22:15:12 +08:00
className='w-full !rounded-lg'
2025-05-23 00:24:08 +08:00
optionList={models}
disabled={!model_limits_enabled}
maxTagCount={3}
/>
2025-06-16 22:15:12 +08:00
<Text type='tertiary' className='mt-1 block text-xs'>
{t('非必要,不建议启用模型限制')}
</Text>
2025-05-23 00:24:08 +08:00
</div>
</div>
</Card>
2025-06-16 22:15:12 +08:00
<Card className='!rounded-2xl shadow-sm border-0'>
{/* Header: Group Info */}
<div className='flex items-center mb-2'>
<Avatar size='small' color='orange' 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>
2025-05-23 00:24:08 +08:00
</div>
</div>
<div>
2025-06-16 22:15:12 +08:00
<Text strong className='block mb-2'>
{t('令牌分组')}
</Text>
2025-05-23 00:24:08 +08:00
{groups.length > 0 ? (
<Select
placeholder={t('令牌分组,默认为用户的分组')}
onChange={(value) => handleInputChange('group', value)}
renderOptionItem={renderGroupOption}
value={inputs.group}
2025-06-16 22:15:12 +08:00
className='w-full !rounded-lg'
2025-05-23 00:24:08 +08:00
optionList={groups}
/>
) : (
<Select
placeholder={t('管理员未设置用户可选分组')}
disabled={true}
2025-06-16 22:15:12 +08:00
className='w-full !rounded-lg'
2025-05-23 00:24:08 +08:00
/>
)}
</div>
</Card>
</div>
</Spin>
</SideSheet>
2024-03-15 16:05:33 +08:00
);
2023-04-23 12:43:10 +08:00
};
export default EditToken;