♻️Refactor: Token Page

This commit is contained in:
Apple\Apple
2025-05-23 00:24:08 +08:00
parent 0f3216564d
commit 67a65213d8
5 changed files with 630 additions and 423 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useContext } from 'react';
import {
API,
copy,
@@ -11,9 +11,8 @@ import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderQuota } from '../helpers/render';
import {
Button,
Divider,
Card,
Dropdown,
Form,
Modal,
Popconfirm,
Popover,
@@ -21,11 +20,28 @@ import {
SplitButtonGroup,
Table,
Tag,
Typography,
Input,
Divider,
} from '@douyinfe/semi-ui';
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
import {
IconPlus,
IconCopy,
IconSearch,
IconTreeTriangleDown,
IconEyeOpened,
IconEdit,
IconDelete,
IconStop,
IconPlay,
IconMore,
} from '@douyinfe/semi-icons';
import EditToken from '../pages/Token/EditToken';
import { useTranslation } from 'react-i18next';
import { UserContext } from '../context/User';
const { Text } = Typography;
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
@@ -33,44 +49,45 @@ function renderTimestamp(timestamp) {
const TokensTable = () => {
const { t } = useTranslation();
const [userState, userDispatch] = useContext(UserContext);
const renderStatus = (status, model_limits_enabled = false) => {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
<Tag color='green' size='large' shape='circle'>
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
<Tag color='green' size='large' shape='circle'>
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
<Tag color='red' size='large' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
<Tag color='yellow' size='large' shape='circle'>
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
<Tag color='grey' size='large' shape='circle'>
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
<Tag color='black' size='large' shape='circle'>
{t('未知状态')}
</Tag>
);
@@ -111,11 +128,11 @@ const TokensTable = () => {
return (
<div>
{record.unlimited_quota ? (
<Tag size={'large'} color={'white'}>
<Tag size={'large'} color={'white'} shape='circle'>
{t('无限制')}
</Tag>
) : (
<Tag size={'large'} color={'light-blue'}>
<Tag size={'large'} color={'light-blue'} shape='circle'>
{renderQuota(parseInt(text))}
</Tag>
)}
@@ -151,16 +168,11 @@ const TokensTable = () => {
if (shouldUseCustom) {
try {
// console.log(chats);
chats = JSON.parse(chats);
// check chats is array
if (Array.isArray(chats)) {
for (let i = 0; i < chats.length; i++) {
let chat = {};
chat.node = 'item';
// c is a map
// chat.key = chats[i].name;
// console.log(chats[i])
for (let key in chats[i]) {
if (chats[i].hasOwnProperty(key)) {
chat.key = i;
@@ -178,33 +190,72 @@ const TokensTable = () => {
showError(t('聊天链接配置错误,请联系管理员'));
}
}
// 创建更多操作的下拉菜单项
const moreMenuItems = [
{
node: 'item',
name: t('查看'),
icon: <IconEyeOpened />,
onClick: () => {
Modal.info({
title: t('令牌详情'),
content: 'sk-' + record.key,
size: 'large',
});
},
},
{
node: 'item',
name: t('删除'),
icon: <IconDelete />,
type: 'danger',
onClick: () => {
Modal.confirm({
title: t('确定是否要删除此令牌?'),
content: t('此修改将不可逆'),
onOk: () => {
manageToken(record.id, 'delete', record).then(() => {
removeRecord(record.key);
});
},
});
},
}
];
// 动态添加启用/禁用按钮
if (record.status === 1) {
moreMenuItems.push({
node: 'item',
name: t('禁用'),
icon: <IconStop />,
type: 'warning',
onClick: () => {
manageToken(record.id, 'disable', record);
},
});
} else {
moreMenuItems.push({
node: 'item',
name: t('启用'),
icon: <IconPlay />,
type: 'secondary',
onClick: () => {
manageToken(record.id, 'enable', record);
},
});
}
return (
<div>
<Popover
content={'sk-' + record.key}
style={{ padding: 20 }}
position='top'
>
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
{t('查看')}
</Button>
</Popover>
<Button
theme='light'
type='secondary'
style={{ marginRight: 1 }}
onClick={async (text) => {
await copyText('sk-' + record.key);
}}
>
{t('复制')}
</Button>
<Space wrap>
<SplitButtonGroup
style={{ marginRight: 1 }}
className="!rounded-full overflow-hidden"
aria-label={t('项目操作按钮组')}
>
<Button
theme='light'
size="small"
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
onClick={() => {
if (chatsArray.length === 0) {
@@ -227,56 +278,35 @@ const TokensTable = () => {
>
<Button
style={{
padding: '8px 4px',
padding: '4px 4px',
color: 'rgba(var(--semi-teal-7), 1)',
}}
type='primary'
icon={<IconTreeTriangleDown />}
size="small"
></Button>
</Dropdown>
</SplitButtonGroup>
<Popconfirm
title={t('确定是否要删除此令牌?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
manageToken(record.id, 'delete', record).then(() => {
removeRecord(record.key);
});
<Button
icon={<IconCopy />}
theme='light'
type='secondary'
size="small"
className="!rounded-full"
onClick={async (text) => {
await copyText('sk-' + record.key);
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
<Button
theme='light'
type='warning'
style={{ marginRight: 1 }}
onClick={async () => {
manageToken(record.id, 'disable', record);
}}
>
{t('禁用')}
</Button>
) : (
<Button
theme='light'
type='secondary'
style={{ marginRight: 1 }}
onClick={async () => {
manageToken(record.id, 'enable', record);
}}
>
{t('启用')}
</Button>
)}
{t('复制')}
</Button>
<Button
icon={<IconEdit />}
theme='light'
type='tertiary'
style={{ marginRight: 1 }}
size="small"
className="!rounded-full"
onClick={() => {
setEditingToken(record);
setShowEdit(true);
@@ -284,7 +314,21 @@ const TokensTable = () => {
>
{t('编辑')}
</Button>
</div>
<Dropdown
trigger='click'
position='bottomRight'
menu={moreMenuItems}
>
<Button
icon={<IconMore />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
/>
</Dropdown>
</Space>
);
},
},
@@ -362,7 +406,6 @@ const TokensTable = () => {
};
const onOpenLink = async (type, url, record) => {
// console.log(type, url, key);
let status = localStorage.getItem('status');
let serverAddress = '';
if (status) {
@@ -379,7 +422,26 @@ const TokensTable = () => {
window.open(url, '_blank');
};
// 获取用户数据
const getUserData = async () => {
try {
const res = await API.get(`/api/user/self`);
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
} else {
showError(message);
}
} catch (error) {
console.error('获取用户数据失败:', error);
showError(t('获取用户数据失败'));
}
};
useEffect(() => {
// 获取用户数据以确保显示正确的余额和使用量
getUserData();
loadTokens(0)
.then()
.catch((reason) => {
@@ -421,11 +483,9 @@ const TokensTable = () => {
showSuccess('操作成功完成!');
let token = res.data.data;
let newTokens = [...tokens];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if (action === 'delete') {
} else {
record.status = token.status;
// newTokens[realIdx].status = token.status;
}
setTokensFormat(newTokens);
} else {
@@ -436,7 +496,6 @@ const TokensTable = () => {
const searchTokens = async () => {
if (searchKeyword === '' && searchToken === '') {
// if keyword is blank, load files instead.
await loadTokens(0);
setActivePage(1);
return;
@@ -480,14 +539,13 @@ const TokensTable = () => {
const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(tokens.length / pageSize) + 1) {
// In this case we have to load more data and then append them.
loadTokens(page - 1).then((r) => {});
loadTokens(page - 1).then((r) => { });
}
};
const rowSelection = {
onSelect: (record, selected) => {},
onSelectAll: (selected, selectedRows) => {},
onSelect: (record, selected) => { },
onSelectAll: (selected, selectedRows) => { },
onChange: (selectedRowKeys, selectedRows) => {
setSelectedKeys(selectedRows);
},
@@ -505,6 +563,117 @@ const TokensTable = () => {
}
};
const renderHeader = () => (
<div className="flex flex-col w-full">
<div className="mb-2">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center text-orange-500">
<IconEyeOpened className="mr-2" />
<Text>{t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')}</Text>
</div>
<div className="flex flex-wrap gap-4 mt-2 md:mt-0">
<div className="flex items-center">
<span className="text-xl mr-2">💰</span>
<div>
<Text type="tertiary" size="small">{t('当前余额')}</Text>
<div className="font-medium">{renderQuota(userState?.user?.quota)}</div>
</div>
</div>
<div className="flex items-center">
<span className="text-xl mr-2">📊</span>
<div>
<Text type="tertiary" size="small">{t('累计消费')}</Text>
<div className="font-medium">{renderQuota(userState?.user?.used_quota)}</div>
</div>
</div>
<div className="flex items-center">
<span className="text-xl mr-2">🔄</span>
<div>
<Text type="tertiary" size="small">{t('请求次数')}</Text>
<div className="font-medium">{userState?.user?.request_count || 0}</div>
</div>
</div>
</div>
</div>
</div>
<Divider margin="12px" />
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
<Button
theme="light"
type="primary"
icon={<IconPlus />}
className="!rounded-full w-full md:w-auto"
onClick={() => {
setEditingToken({
id: undefined,
});
setShowEdit(true);
}}
>
{t('添加令牌')}
</Button>
<Button
theme="light"
type="warning"
icon={<IconCopy />}
className="!rounded-full w-full md:w-auto"
onClick={async () => {
if (selectedKeys.length === 0) {
showError(t('请至少选择一个令牌!'));
return;
}
let keys = '';
for (let i = 0; i < selectedKeys.length; i++) {
keys +=
selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
}
await copyText(keys);
}}
>
{t('复制所选')}
</Button>
</div>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
<div className="relative w-full md:w-56">
<Input
prefix={<IconSearch />}
placeholder={t('搜索关键字')}
value={searchKeyword}
onChange={handleKeywordChange}
className="!rounded-full"
showClear
/>
</div>
<div className="relative w-full md:w-56">
<Input
prefix={<IconSearch />}
placeholder={t('密钥')}
value={searchToken}
onChange={handleSearchTokenChange}
className="!rounded-full"
showClear
/>
</div>
<Button
type="primary"
onClick={searchTokens}
loading={searching}
className="!rounded-full w-full md:w-auto"
>
{t('查询')}
</Button>
</div>
</div>
</div>
);
return (
<>
<EditToken
@@ -513,99 +682,40 @@ const TokensTable = () => {
visiable={showEdit}
handleClose={closeEdit}
></EditToken>
<Form
layout='horizontal'
style={{ marginTop: 10 }}
labelPosition={'left'}
>
<Form.Input
field='keyword'
label={t('搜索关键字')}
placeholder={t('令牌名称')}
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
/>
<Form.Input
field='token'
label={t('密钥')}
placeholder={t('密钥')}
value={searchToken}
loading={searching}
onChange={handleSearchTokenChange}
/>
<Button
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={searchTokens}
style={{ marginRight: 8 }}
>
{t('查询')}
</Button>
</Form>
<Divider style={{ margin: '15px 0' }} />
<div>
<Button
theme='light'
type='primary'
style={{ marginRight: 8 }}
onClick={() => {
setEditingToken({
id: undefined,
});
setShowEdit(true);
}}
>
{t('添加令牌')}
</Button>
<Button
label={t('复制所选令牌')}
type='warning'
onClick={async () => {
if (selectedKeys.length === 0) {
showError(t('请至少选择一个令牌!'));
return;
}
let keys = '';
for (let i = 0; i < selectedKeys.length; i++) {
keys +=
selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
}
await copyText(keys);
}}
>
{t('复制所选令牌到剪贴板')}
</Button>
</div>
<Table
style={{ marginTop: 20 }}
columns={columns}
dataSource={pageData}
pagination={{
currentPage: activePage,
pageSize: pageSize,
total: tokenCount,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: tokens.length,
}),
onPageSizeChange: (size) => {
setPageSize(size);
setActivePage(1);
},
onPageChange: handlePageChange,
}}
loading={loading}
rowSelection={rowSelection}
onRow={handleRow}
></Table>
<Card
className="!rounded-2xl overflow-hidden"
title={renderHeader()}
shadows='hover'
>
<Table
columns={columns}
dataSource={pageData}
pagination={{
currentPage: activePage,
pageSize: pageSize,
total: tokenCount,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: tokens.length,
}),
onPageSizeChange: (size) => {
setPageSize(size);
setActivePage(1);
},
onPageChange: handlePageChange,
}}
loading={loading}
rowSelection={rowSelection}
onRow={handleRow}
className="rounded-xl overflow-hidden"
size="middle"
></Table>
</Card>
</>
);
};

View File

@@ -17,7 +17,7 @@ export function renderText(text, limit) {
export function renderGroup(group) {
if (group === '') {
return (
<Tag size='large' key='default' color='orange'>
<Tag size='large' key='default' color='orange' shape='circle'>
{i18next.t('用户分组')}
</Tag>
);
@@ -39,6 +39,7 @@ export function renderGroup(group) {
size='large'
color={tagColors[group] || stringToColor(group)}
key={group}
shape='circle'
onClick={async (event) => {
event.stopPropagation();
if (await copy(group)) {

View File

@@ -448,7 +448,7 @@
"一分钟后过期": "Expires after one minute",
"创建新的令牌": "Create New Token",
"令牌分组,默认为用户的分组": "Token group, default is the your's group",
"IP白名单(请勿过度信任此功能)": "IP whitelist (do not overly trust this function)",
"IP白名单": "IP whitelist",
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
"设为无限额度": "Set to unlimited quota",
"更新令牌信息": "Update Token Information",
@@ -895,7 +895,7 @@
"渠道分组": "Channel grouping",
"安全设置(可选)": "Security settings (optional)",
"IP 限制": "IP restrictions",
"启用模型限制(非必要,不建议启用)": "Enable model restrictions (not necessary, not recommended)",
"模型限制": "Model restrictions",
"秒": "Second",
"更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.",
"一小时": "One hour",
@@ -1410,5 +1410,23 @@
"早上好": "Good morning",
"中午好": "Good afternoon",
"下午好": "Good afternoon",
"晚上好": "Good evening"
"晚上好": "Good evening",
"更多提示信息": "More Prompts",
"新建": "Create",
"更新": "Update",
"基本信息": "Basic Information",
"设置令牌的基本信息": "Set token basic information",
"设置令牌可用额度和数量": "Set token available quota and quantity",
"访问限制": "Access Restrictions",
"设置令牌的访问限制": "Set token access restrictions",
"请勿过度信任此功能IP可能被伪造": "Do not over-trust this feature, IP can be spoofed",
"勾选启用模型限制后可选择": "Select after checking to enable model restrictions",
"非必要,不建议启用模型限制": "Not necessary, model restrictions are not recommended",
"分组信息": "Group Information",
"设置令牌的分组": "Set token grouping",
"管理员未设置用户可选分组": "Administrator has not set user-selectable groups",
"10个": "10 items",
"20个": "20 items",
"30个": "30 items",
"100个": "100 items"
}

View File

@@ -21,12 +21,26 @@ import {
Spin,
TextArea,
Typography,
Card,
Tag,
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
import {
IconClock,
IconCalendar,
IconCreditCard,
IconLink,
IconServer,
IconUserGroup,
IconSave,
IconClose,
IconPlusCircle,
} from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next';
const { Text, Title } = Typography;
const EditToken = (props) => {
const { t } = useTranslation();
const [isEdit, setIsEdit] = useState(false);
const [loading, setLoading] = useState(isEdit);
const originInputs = {
@@ -50,17 +64,18 @@ const EditToken = (props) => {
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;
@@ -128,6 +143,7 @@ const EditToken = (props) => {
}
setLoading(false);
};
useEffect(() => {
setIsEdit(props.editingToken.id !== undefined);
}, [props.editingToken.id]);
@@ -241,237 +257,312 @@ const EditToken = (props) => {
};
return (
<>
<SideSheet
placement={isEdit ? 'right' : 'left'}
title={
<Title level={3}>
<SideSheet
placement={isEdit ? 'right' : 'left'}
title={
<Space>
{isEdit ?
<Tag color="blue" shape="circle">{t('更新')}</Tag> :
<Tag color="green" shape="circle">{t('新建')}</Tag>
}
<Title heading={4} className="m-0">
{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>
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
padding: '24px'
}}
bodyStyle={{
backgroundColor: 'var(--semi-color-bg-0)',
padding: '0'
}}
visible={props.visiable}
width={isMobile() ? '100%' : 600}
footer={
<div className="flex justify-end bg-white">
<Space>
<Button
style={{ marginTop: 8 }}
type={'warning'}
onClick={() => {
setUnlimitedQuota();
}}
theme="solid"
size="large"
className="!rounded-full"
onClick={submit}
icon={<IconSave />}
loading={loading}
>
{unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
{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>
<Button
theme="light"
size="large"
className="!rounded-full"
type="primary"
onClick={handleCancel}
icon={<IconClose />}
>
{t('取消')}
</Button>
</Space>
</div>
}
closeIcon={null}
onCancel={() => handleCancel()}
>
<Spin spinning={loading}>
<div className="p-6">
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-4">
<IconPlusCircle size="large" className="text-blue-500" />
</div>
<div>
<Text className="text-lg font-medium">{t('基本信息')}</Text>
<div className="text-gray-500 text-sm">{t('设置令牌的基本信息')}</div>
</div>
</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);
}}
position={'topLeft'}
renderOptionItem={renderGroupOption}
value={inputs.group}
autoComplete='new-password'
optionList={groups}
<div className="space-y-4">
<div>
<Text strong className="block mb-2">{t('名称')}</Text>
<Input
placeholder={t('请输入名称')}
onChange={(value) => handleInputChange('name', value)}
value={name}
autoComplete="new-password"
size="large"
className="!rounded-lg"
showClear
required
/>
</div>
<div>
<Text strong className="block mb-2">{t('过期时间')}</Text>
<div className="mb-2">
<DatePicker
placeholder={t('请选择过期时间')}
onChange={(value) => handleInputChange('expired_time', value)}
value={expired_time}
autoComplete="new-password"
type="dateTime"
className="w-full !rounded-lg"
size="large"
prefix={<IconCalendar />}
/>
</div>
<div className="flex flex-wrap gap-2">
<Button
theme="light"
type="primary"
onClick={() => setExpiredTime(0, 0, 0, 0)}
className="!rounded-full"
>
{t('永不过期')}
</Button>
<Button
theme="light"
type="tertiary"
onClick={() => setExpiredTime(0, 0, 1, 0)}
className="!rounded-full"
icon={<IconClock />}
>
{t('一小时')}
</Button>
<Button
theme="light"
type="tertiary"
onClick={() => setExpiredTime(0, 1, 0, 0)}
className="!rounded-full"
icon={<IconCalendar />}
>
{t('一天')}
</Button>
<Button
theme="light"
type="tertiary"
onClick={() => setExpiredTime(1, 0, 0, 0)}
className="!rounded-full"
icon={<IconCalendar />}
>
{t('一个月')}
</Button>
</div>
</div>
</div>
</Card>
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-4">
<IconCreditCard size="large" className="text-green-500" />
</div>
<div>
<Text className="text-lg font-medium">{t('额度设置')}</Text>
<div className="text-gray-500 text-sm">{t('设置令牌可用额度和数量')}</div>
</div>
</div>
<Banner
type="warning"
description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
className="mb-4 !rounded-lg"
/>
) : (
<Select
style={{ marginTop: 8 }}
placeholder={t('管理员未设置用户可选分组')}
name='gruop'
disabled={true}
/>
)}
</Spin>
</SideSheet>
</>
<div className="space-y-4">
<div>
<div className="flex justify-between mb-2">
<Text strong>{t('额度')}</Text>
<Text type="tertiary">{renderQuotaWithPrompt(remain_quota)}</Text>
</div>
<AutoComplete
placeholder={t('请输入额度')}
onChange={(value) => handleInputChange('remain_quota', value)}
value={remain_quota}
autoComplete="new-password"
type="number"
size="large"
className="w-full !rounded-lg"
prefix={<IconCreditCard />}
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>
<Text strong className="block mb-2">{t('新建数量')}</Text>
<AutoComplete
placeholder={t('请选择或输入创建令牌的数量')}
onChange={(value) => handleTokenCountChange(value)}
onSelect={(value) => handleTokenCountChange(value)}
value={tokenCount.toString()}
autoComplete="off"
type="number"
className="w-full !rounded-lg"
size="large"
prefix={<IconPlusCircle />}
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>
)}
<div className="flex justify-end">
<Button
theme="light"
type={unlimited_quota ? "danger" : "warning"}
onClick={setUnlimitedQuota}
className="!rounded-full"
>
{unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
</Button>
</div>
</div>
</Card>
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-4">
<IconLink size="large" className="text-purple-500" />
</div>
<div>
<Text className="text-lg font-medium">{t('访问限制')}</Text>
<div className="text-gray-500 text-sm">{t('设置令牌的访问限制')}</div>
</div>
</div>
<div className="space-y-4">
<div>
<Text strong className="block mb-2">{t('IP白名单')}</Text>
<TextArea
placeholder={t('允许的IP一行一个不填写则不限制')}
onChange={(value) => handleInputChange('allow_ips', value)}
value={inputs.allow_ips}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
className="!rounded-lg"
rows={4}
/>
<Text type="tertiary" className="mt-1 block text-xs">{t('请勿过度信任此功能IP可能被伪造')}</Text>
</div>
<div>
<div className="flex items-center mb-2">
<Checkbox
checked={model_limits_enabled}
onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
>
<Text strong>{t('模型限制')}</Text>
</Checkbox>
</div>
<Select
placeholder={model_limits_enabled ? t('请选择该渠道所支持的模型') : t('勾选启用模型限制后可选择')}
onChange={(value) => handleInputChange('model_limits', value)}
value={inputs.model_limits}
multiple
size="large"
className="w-full !rounded-lg"
prefix={<IconServer />}
optionList={models}
disabled={!model_limits_enabled}
maxTagCount={3}
/>
<Text type="tertiary" className="mt-1 block text-xs">{t('非必要,不建议启用模型限制')}</Text>
</div>
</div>
</Card>
<Card className="!rounded-2xl shadow-sm border-0">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-4">
<IconUserGroup size="large" className="text-orange-500" />
</div>
<div>
<Text className="text-lg font-medium">{t('分组信息')}</Text>
<div className="text-gray-500 text-sm">{t('设置令牌的分组')}</div>
</div>
</div>
<div>
<Text strong className="block mb-2">{t('令牌分组')}</Text>
{groups.length > 0 ? (
<Select
placeholder={t('令牌分组,默认为用户的分组')}
onChange={(value) => handleInputChange('group', value)}
renderOptionItem={renderGroupOption}
value={inputs.group}
size="large"
className="w-full !rounded-lg"
prefix={<IconUserGroup />}
optionList={groups}
/>
) : (
<Select
placeholder={t('管理员未设置用户可选分组')}
disabled={true}
size="large"
className="w-full !rounded-lg"
prefix={<IconUserGroup />}
/>
)}
</div>
</Card>
</div>
</Spin>
</SideSheet>
);
};

View File

@@ -1,24 +1,11 @@
import React from 'react';
import TokensTable from '../../components/TokensTable';
import { Banner, Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Token = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<Banner
type='warning'
description={t(
'令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。',
)}
/>
</Layout.Header>
<Layout.Content>
<TokensTable />
</Layout.Content>
</Layout>
<TokensTable />
</>
);
};