mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-17 01:06:37 +08:00
feat: i18n for token related pages
This commit is contained in:
parent
93ce6c4cd7
commit
60f2776795
@ -152,5 +152,93 @@
|
||||
"tencent": "Enter in format: AppId|SecretId|SecretKey"
|
||||
}
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"title": "Token Management",
|
||||
"search": "Search tokens by name ...",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"used_quota": "Used Quota",
|
||||
"remain_quota": "Remaining Quota",
|
||||
"created_time": "Created Time",
|
||||
"expired_time": "Expiry Time",
|
||||
"actions": "Actions",
|
||||
"no_name": "None",
|
||||
"never_expire": "Never Expires",
|
||||
"unlimited": "Unlimited",
|
||||
"status_enabled": "Enabled",
|
||||
"status_disabled": "Disabled",
|
||||
"status_expired": "Expired",
|
||||
"status_depleted": "Depleted",
|
||||
"status_unknown": "Unknown Status"
|
||||
},
|
||||
"buttons": {
|
||||
"copy": "Copy",
|
||||
"chat": "Chat",
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Delete Token",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"edit": "Edit",
|
||||
"add": "Add New Token",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"edit": {
|
||||
"title_edit": "Update Token Information",
|
||||
"title_create": "Create New Token",
|
||||
"name": "Name",
|
||||
"name_placeholder": "Please enter name",
|
||||
"models": "Model Scope",
|
||||
"models_placeholder": "Please select allowed models, leave empty for no restrictions",
|
||||
"ip_limit": "IP Restriction",
|
||||
"ip_limit_placeholder": "Please enter allowed subnets, e.g.: 192.168.0.0/24, use commas to separate multiple subnets",
|
||||
"expire_time": "Expiry Time",
|
||||
"expire_time_placeholder": "Please enter expiry time in yyyy-MM-dd HH:mm:ss format, -1 for no limit",
|
||||
"quota_notice": "Note: Token quota only limits the maximum usage of the token itself, actual usage is subject to account remaining quota.",
|
||||
"quota": "Quota",
|
||||
"quota_placeholder": "Please enter quota",
|
||||
"buttons": {
|
||||
"never_expire": "Never Expire",
|
||||
"expire_1_month": "Expire in 1 Month",
|
||||
"expire_1_day": "Expire in 1 Day",
|
||||
"expire_1_hour": "Expire in 1 Hour",
|
||||
"expire_1_minute": "Expire in 1 Minute",
|
||||
"unlimited_quota": "Set Unlimited Quota",
|
||||
"cancel_unlimited": "Cancel Unlimited Quota",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "Token updated successfully!",
|
||||
"create_success": "Token created successfully, please copy it from the list page!",
|
||||
"expire_time_invalid": "Invalid expiry time format!"
|
||||
}
|
||||
},
|
||||
"copy_options": {
|
||||
"raw": "Copy Raw Token",
|
||||
"ama": "Copy AMA Link",
|
||||
"opencat": "Copy OpenCat Link",
|
||||
"next": "Copy NextChat Link",
|
||||
"lobe": "Copy LobeChat Link"
|
||||
},
|
||||
"messages": {
|
||||
"copy_success": "Copied to clipboard!",
|
||||
"copy_failed": "Unable to copy to clipboard, please copy manually. Token has been filled in the search box.",
|
||||
"operation_success": "Operation completed successfully!"
|
||||
},
|
||||
"sort": {
|
||||
"placeholder": "Sort By",
|
||||
"default": "Default Order",
|
||||
"by_remain": "Sort by Remaining Quota",
|
||||
"by_used": "Sort by Used Quota"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"quota": {
|
||||
"display": "Equivalent: ${{amount}}",
|
||||
"display_short": "${{amount}}",
|
||||
"unit": "$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,5 +152,93 @@
|
||||
"tencent": "按照如下格式输入:AppId|SecretId|SecretKey"
|
||||
}
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"title": "令牌管理",
|
||||
"search": "搜索令牌的名称 ...",
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"status": "状态",
|
||||
"used_quota": "已用额度",
|
||||
"remain_quota": "剩余额度",
|
||||
"created_time": "创建时间",
|
||||
"expired_time": "过期时间",
|
||||
"actions": "操作",
|
||||
"no_name": "无",
|
||||
"never_expire": "永不过期",
|
||||
"unlimited": "无限制",
|
||||
"status_enabled": "已启用",
|
||||
"status_disabled": "已禁用",
|
||||
"status_expired": "已过期",
|
||||
"status_depleted": "已耗尽",
|
||||
"status_unknown": "未知状态"
|
||||
},
|
||||
"buttons": {
|
||||
"copy": "复制",
|
||||
"chat": "聊天",
|
||||
"delete": "删除",
|
||||
"confirm_delete": "删除令牌",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"edit": "编辑",
|
||||
"add": "添加新的令牌",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"edit": {
|
||||
"title_edit": "更新令牌信息",
|
||||
"title_create": "创建新的令牌",
|
||||
"name": "名称",
|
||||
"name_placeholder": "请输入名称",
|
||||
"models": "模型范围",
|
||||
"models_placeholder": "请选择允许使用的模型,留空则不进行限制",
|
||||
"ip_limit": "IP 限制",
|
||||
"ip_limit_placeholder": "请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段",
|
||||
"expire_time": "过期时间",
|
||||
"expire_time_placeholder": "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制",
|
||||
"quota_notice": "注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。",
|
||||
"quota": "额度",
|
||||
"quota_placeholder": "请输入额度",
|
||||
"buttons": {
|
||||
"never_expire": "永不过期",
|
||||
"expire_1_month": "一个月后过期",
|
||||
"expire_1_day": "一天后过期",
|
||||
"expire_1_hour": "一小时后过期",
|
||||
"expire_1_minute": "一分钟后过期",
|
||||
"unlimited_quota": "设为无限额度",
|
||||
"cancel_unlimited": "取消无限额度",
|
||||
"submit": "提交",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "令牌更新成功!",
|
||||
"create_success": "令牌创建成功,请在列表页面点击复制获取令牌!",
|
||||
"expire_time_invalid": "过期时间格式错误!"
|
||||
}
|
||||
},
|
||||
"copy_options": {
|
||||
"raw": "复制原始令牌",
|
||||
"ama": "复制 AMA 链接",
|
||||
"opencat": "复制 OpenCat 链接",
|
||||
"next": "复制 NextChat 链接",
|
||||
"lobe": "复制 LobeChat 链接"
|
||||
},
|
||||
"messages": {
|
||||
"copy_success": "已复制到剪贴板!",
|
||||
"copy_failed": "无法复制到剪贴板,请手动复制,已将令牌填入搜索框。",
|
||||
"operation_success": "操作成功完成!"
|
||||
},
|
||||
"sort": {
|
||||
"placeholder": "排序方式",
|
||||
"default": "默认排序",
|
||||
"by_remain": "按剩余额度排序",
|
||||
"by_used": "按已用额度排序"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"quota": {
|
||||
"display": "等价金额:${{amount}}",
|
||||
"display_short": "${{amount}}",
|
||||
"unit": "$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,32 +42,36 @@ function App() {
|
||||
}
|
||||
};
|
||||
const loadStatus = async () => {
|
||||
const res = await API.get('/api/status');
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
localStorage.setItem('system_name', data.system_name);
|
||||
localStorage.setItem('logo', data.logo);
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
localStorage.setItem('quota_per_unit', data.quota_per_unit);
|
||||
localStorage.setItem('display_in_currency', data.display_in_currency);
|
||||
if (data.chat_link) {
|
||||
localStorage.setItem('chat_link', data.chat_link);
|
||||
try {
|
||||
const res = await API.get('/api/status');
|
||||
const { success, message, data } = res.data || {}; // Add default empty object
|
||||
if (success && data) { // Check data exists
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
localStorage.setItem('system_name', data.system_name);
|
||||
localStorage.setItem('logo', data.logo);
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
localStorage.setItem('quota_per_unit', data.quota_per_unit);
|
||||
localStorage.setItem('display_in_currency', data.display_in_currency);
|
||||
if (data.chat_link) {
|
||||
localStorage.setItem('chat_link', data.chat_link);
|
||||
} else {
|
||||
localStorage.removeItem('chat_link');
|
||||
}
|
||||
if (
|
||||
data.version !== process.env.REACT_APP_VERSION &&
|
||||
data.version !== 'v0.0.0' &&
|
||||
process.env.REACT_APP_VERSION !== ''
|
||||
) {
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('chat_link');
|
||||
showError(message || '无法正常连接至服务器!');
|
||||
}
|
||||
if (
|
||||
data.version !== process.env.REACT_APP_VERSION &&
|
||||
data.version !== 'v0.0.0' &&
|
||||
process.env.REACT_APP_VERSION !== ''
|
||||
) {
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showError('无法正常连接至服务器!');
|
||||
} catch (error) {
|
||||
showError(error.message || '无法正常连接至服务器!');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
@ -21,64 +22,63 @@ import {
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
|
||||
{ key: 'ama', text: 'BotGem', value: 'ama' },
|
||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||
{ key: 'lobechat', text: 'LobeChat', value: 'lobechat' },
|
||||
];
|
||||
|
||||
const OPEN_LINK_OPTIONS = [
|
||||
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
|
||||
{ key: 'ama', text: 'BotGem', value: 'ama' },
|
||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||
{ key: 'lobechat', text: 'LobeChat', value: 'lobechat' },
|
||||
];
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
function renderStatus(status, t) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Label basic color='green'>
|
||||
已启用
|
||||
{t('token.table.status_enabled')}
|
||||
</Label>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Label basic color='red'>
|
||||
{' '}
|
||||
已禁用{' '}
|
||||
{t('token.table.status_disabled')}
|
||||
</Label>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Label basic color='yellow'>
|
||||
{' '}
|
||||
已过期{' '}
|
||||
{t('token.table.status_expired')}
|
||||
</Label>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
{' '}
|
||||
已耗尽{' '}
|
||||
{t('token.table.status_depleted')}
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Label basic color='black'>
|
||||
{' '}
|
||||
未知状态{' '}
|
||||
{t('token.table.status_unknown')}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TokensTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{ key: 'raw', text: t('token.copy_options.raw'), value: '' },
|
||||
{ key: 'next', text: t('token.copy_options.next'), value: 'next' },
|
||||
{ key: 'ama', text: t('token.copy_options.ama'), value: 'ama' },
|
||||
{ key: 'opencat', text: t('token.copy_options.opencat'), value: 'opencat' },
|
||||
{ key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' }
|
||||
];
|
||||
|
||||
const OPEN_LINK_OPTIONS = [
|
||||
{ key: 'next', text: t('token.copy_options.next'), value: 'next' },
|
||||
{ key: 'ama', text: t('token.copy_options.ama'), value: 'ama' },
|
||||
{ key: 'opencat', text: t('token.copy_options.opencat'), value: 'opencat' },
|
||||
{ key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' }
|
||||
];
|
||||
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
@ -135,8 +135,7 @@ const TokensTable = () => {
|
||||
let nextUrl;
|
||||
|
||||
if (nextLink) {
|
||||
nextUrl =
|
||||
nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
} else {
|
||||
nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
}
|
||||
@ -153,17 +152,15 @@ const TokensTable = () => {
|
||||
url = nextUrl;
|
||||
break;
|
||||
case 'lobechat':
|
||||
url =
|
||||
nextLink +
|
||||
`/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
|
||||
url = nextLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
|
||||
break;
|
||||
default:
|
||||
url = `sk-${key}`;
|
||||
}
|
||||
if (await copy(url)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
showSuccess(t('token.messages.copy_success'));
|
||||
} else {
|
||||
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
||||
showWarning(t('token.messages.copy_failed'));
|
||||
setSearchKeyword(url);
|
||||
}
|
||||
};
|
||||
@ -237,7 +234,7 @@ const TokensTable = () => {
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('token.messages.operation_success'));
|
||||
let token = res.data.data;
|
||||
let newTokens = [...tokens];
|
||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
@ -308,7 +305,7 @@ const TokensTable = () => {
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索令牌的名称 ...'
|
||||
placeholder={t('token.search')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@ -324,7 +321,7 @@ const TokensTable = () => {
|
||||
sortToken('name');
|
||||
}}
|
||||
>
|
||||
名称
|
||||
{t('token.table.name')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -332,7 +329,7 @@ const TokensTable = () => {
|
||||
sortToken('status');
|
||||
}}
|
||||
>
|
||||
状态
|
||||
{t('token.table.status')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -340,7 +337,7 @@ const TokensTable = () => {
|
||||
sortToken('used_quota');
|
||||
}}
|
||||
>
|
||||
已用额度
|
||||
{t('token.table.used_quota')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -348,7 +345,7 @@ const TokensTable = () => {
|
||||
sortToken('remain_quota');
|
||||
}}
|
||||
>
|
||||
剩余额度
|
||||
{t('token.table.remain_quota')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -356,7 +353,7 @@ const TokensTable = () => {
|
||||
sortToken('created_time');
|
||||
}}
|
||||
>
|
||||
创建时间
|
||||
{t('token.table.created_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -364,9 +361,9 @@ const TokensTable = () => {
|
||||
sortToken('expired_time');
|
||||
}}
|
||||
>
|
||||
过期时间
|
||||
{t('token.table.expired_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
<Table.HeaderCell>{t('token.table.actions')}</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
@ -378,20 +375,37 @@ const TokensTable = () => {
|
||||
)
|
||||
.map((token, idx) => {
|
||||
if (token.deleted) return <></>;
|
||||
|
||||
const copyOptionsWithHandlers = COPY_OPTIONS.map(option => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onCopy(option.value, token.key);
|
||||
}
|
||||
}));
|
||||
|
||||
const openLinkOptionsWithHandlers = OPEN_LINK_OPTIONS.map(option => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onOpenLink(option.value, token.key);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<Table.Row key={token.id}>
|
||||
<Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(token.status)}</Table.Cell>
|
||||
<Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{token.name ? token.name : t('token.table.no_name')}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderStatus(token.status, t)}</Table.Cell>
|
||||
<Table.Cell>{renderQuota(token.used_quota, t)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{token.unlimited_quota
|
||||
? '无限制'
|
||||
: renderQuota(token.remain_quota, 2)}
|
||||
? t('token.table.unlimited')
|
||||
: renderQuota(token.remain_quota, t, 2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{token.expired_time === -1
|
||||
? '永不过期'
|
||||
? t('token.table.never_expire')
|
||||
: renderTimestamp(token.expired_time)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
@ -400,21 +414,14 @@ const TokensTable = () => {
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={async () => {
|
||||
await onCopy('', token.key);
|
||||
}}
|
||||
onClick={async () => await onCopy('', token.key)}
|
||||
>
|
||||
复制
|
||||
{t('token.buttons.copy')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
className='button icon'
|
||||
floating
|
||||
options={COPY_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onCopy(option.value, token.key);
|
||||
},
|
||||
}))}
|
||||
options={copyOptionsWithHandlers}
|
||||
trigger={<></>}
|
||||
/>
|
||||
</Button.Group>{' '}
|
||||
@ -422,28 +429,21 @@ const TokensTable = () => {
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={() => {
|
||||
onOpenLink('', token.key);
|
||||
}}
|
||||
onClick={() => onOpenLink('', token.key)}
|
||||
>
|
||||
聊天
|
||||
{t('token.buttons.chat')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
className='button icon'
|
||||
floating
|
||||
options={OPEN_LINK_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onOpenLink(option.value, token.key);
|
||||
},
|
||||
}))}
|
||||
options={openLinkOptionsWithHandlers}
|
||||
trigger={<></>}
|
||||
/>
|
||||
</Button.Group>{' '}
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' negative>
|
||||
删除
|
||||
{t('token.buttons.delete')}
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
@ -456,7 +456,7 @@ const TokensTable = () => {
|
||||
manageToken(token.id, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
删除令牌 {token.name}
|
||||
{t('token.buttons.confirm_delete')} {token.name}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button
|
||||
@ -469,14 +469,12 @@ const TokensTable = () => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{token.status === 1 ? '禁用' : '启用'}
|
||||
{token.status === 1
|
||||
? t('token.buttons.disable')
|
||||
: t('token.buttons.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
as={Link}
|
||||
to={'/token/edit/' + token.id}
|
||||
>
|
||||
编辑
|
||||
<Button size={'small'} as={Link} to={'/token/edit/' + token.id}>
|
||||
{t('token.buttons.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
@ -489,24 +487,24 @@ const TokensTable = () => {
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan='7'>
|
||||
<Button size='small' as={Link} to='/token/add' loading={loading}>
|
||||
添加新的令牌
|
||||
{t('token.buttons.add')}
|
||||
</Button>
|
||||
<Button size='small' onClick={refresh} loading={loading}>
|
||||
刷新
|
||||
{t('token.buttons.refresh')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
placeholder='排序方式'
|
||||
placeholder={t('token.sort.placeholder')}
|
||||
selection
|
||||
options={[
|
||||
{ key: '', text: '默认排序', value: '' },
|
||||
{ key: '', text: t('token.sort.default'), value: '' },
|
||||
{
|
||||
key: 'remain_quota',
|
||||
text: '按剩余额度排序',
|
||||
text: t('token.sort.by_remain'),
|
||||
value: 'remain_quota',
|
||||
},
|
||||
{
|
||||
key: 'used_quota',
|
||||
text: '按已用额度排序',
|
||||
text: t('token.sort.by_used'),
|
||||
value: 'used_quota',
|
||||
},
|
||||
]}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Label } from 'semantic-ui-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function renderText(text, limit) {
|
||||
if (text.length > limit) {
|
||||
@ -39,23 +40,27 @@ export function renderNumber(num) {
|
||||
}
|
||||
}
|
||||
|
||||
export function renderQuota(quota, digits = 2) {
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
quotaPerUnit = parseFloat(quotaPerUnit);
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
export function renderQuota(quota, t, precision = 2) {
|
||||
const displayInCurrency = localStorage.getItem('display_in_currency') === 'true';
|
||||
const quotaPerUnit = parseFloat(localStorage.getItem('quota_per_unit') || '1');
|
||||
|
||||
if (displayInCurrency) {
|
||||
return '$' + (quota / quotaPerUnit).toFixed(digits);
|
||||
const amount = (quota / quotaPerUnit).toFixed(precision);
|
||||
return t('common.quota.display_short', { amount });
|
||||
}
|
||||
|
||||
return renderNumber(quota);
|
||||
}
|
||||
|
||||
export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
export function renderQuotaWithPrompt(quota, t) {
|
||||
const displayInCurrency = localStorage.getItem('display_in_currency') === 'true';
|
||||
const quotaPerUnit = parseFloat(localStorage.getItem('quota_per_unit') || '1');
|
||||
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
const amount = (quota / quotaPerUnit).toFixed(2);
|
||||
return ` (${t('common.quota.display', { amount })})`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
import { renderQuotaWithPrompt } from '../../helpers/render';
|
||||
|
||||
const EditToken = () => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const tokenId = params.id;
|
||||
const isEdit = tokenId !== undefined;
|
||||
@ -60,47 +62,61 @@ const EditToken = () => {
|
||||
};
|
||||
|
||||
const loadToken = async () => {
|
||||
let res = await API.get(`/api/token/${tokenId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (data.expired_time !== -1) {
|
||||
data.expired_time = timestamp2string(data.expired_time);
|
||||
}
|
||||
if (data.models === '') {
|
||||
data.models = [];
|
||||
try {
|
||||
let res = await API.get(`/api/token/${tokenId}`);
|
||||
const { success, message, data } = res.data || {};
|
||||
if (success && data) {
|
||||
if (data.expired_time !== -1) {
|
||||
data.expired_time = timestamp2string(data.expired_time);
|
||||
}
|
||||
if (data.models === '') {
|
||||
data.models = [];
|
||||
} else {
|
||||
data.models = data.models.split(',');
|
||||
}
|
||||
setInputs(data);
|
||||
} else {
|
||||
data.models = data.models.split(',');
|
||||
showError(message || 'Failed to load token');
|
||||
}
|
||||
setInputs(data);
|
||||
} else {
|
||||
showError(message);
|
||||
} catch (error) {
|
||||
showError(error.message || 'Network error');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
loadToken().then();
|
||||
}
|
||||
loadAvailableModels().then();
|
||||
}, []);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
let res = await API.get(`/api/user/available_models`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let options = data.map((model) => {
|
||||
return {
|
||||
key: model,
|
||||
text: model,
|
||||
value: model,
|
||||
};
|
||||
});
|
||||
setModelOptions(options);
|
||||
} else {
|
||||
showError(message);
|
||||
try {
|
||||
let res = await API.get(`/api/user/available_models`);
|
||||
const { success, message, data } = res.data || {};
|
||||
if (success && data) {
|
||||
let options = data.map((model) => {
|
||||
return {
|
||||
key: model,
|
||||
text: model,
|
||||
value: model,
|
||||
};
|
||||
});
|
||||
setModelOptions(options);
|
||||
} else {
|
||||
showError(message || 'Failed to load models');
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error.message || 'Network error');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
loadToken().catch(error => {
|
||||
showError(error.message || 'Failed to load token');
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
loadAvailableModels().catch(error => {
|
||||
showError(error.message || 'Failed to load models');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
if (!isEdit && inputs.name === '') return;
|
||||
let localInputs = inputs;
|
||||
@ -108,7 +124,7 @@ const EditToken = () => {
|
||||
if (localInputs.expired_time !== -1) {
|
||||
let time = Date.parse(localInputs.expired_time);
|
||||
if (isNaN(time)) {
|
||||
showError('过期时间格式错误!');
|
||||
showError(t('token.edit.messages.expire_time_invalid'));
|
||||
return;
|
||||
}
|
||||
localInputs.expired_time = Math.ceil(time / 1000);
|
||||
@ -126,9 +142,9 @@ const EditToken = () => {
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
if (isEdit) {
|
||||
showSuccess('令牌更新成功!');
|
||||
showSuccess(t('token.edit.messages.update_success'));
|
||||
} else {
|
||||
showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!');
|
||||
showSuccess(t('token.edit.messages.create_success'));
|
||||
setInputs(originInputs);
|
||||
}
|
||||
} else {
|
||||
@ -141,14 +157,14 @@ const EditToken = () => {
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header className='header'>
|
||||
{isEdit ? '更新令牌信息' : '创建新的令牌'}
|
||||
{isEdit ? t('token.edit.title_edit') : t('token.edit.title_create')}
|
||||
</Card.Header>
|
||||
<Form loading={loading} autoComplete='new-password'>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='名称'
|
||||
label={t('token.edit.name')}
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
placeholder={t('token.edit.name_placeholder')}
|
||||
onChange={handleInputChange}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
@ -157,8 +173,8 @@ const EditToken = () => {
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Dropdown
|
||||
label='模型范围'
|
||||
placeholder={'请选择允许使用的模型,留空则不进行限制'}
|
||||
label={t('token.edit.models')}
|
||||
placeholder={t('token.edit.models_placeholder')}
|
||||
name='models'
|
||||
fluid
|
||||
multiple
|
||||
@ -175,11 +191,9 @@ const EditToken = () => {
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='IP 限制'
|
||||
label={t('token.edit.ip_limit')}
|
||||
name='subnet'
|
||||
placeholder={
|
||||
'请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段'
|
||||
}
|
||||
placeholder={t('token.edit.ip_limit_placeholder')}
|
||||
onChange={handleInputChange}
|
||||
value={inputs.subnet}
|
||||
autoComplete='new-password'
|
||||
@ -187,11 +201,9 @@ const EditToken = () => {
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='过期时间'
|
||||
label={t('token.edit.expire_time')}
|
||||
name='expired_time'
|
||||
placeholder={
|
||||
'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'
|
||||
}
|
||||
placeholder={t('token.edit.expire_time_placeholder')}
|
||||
onChange={handleInputChange}
|
||||
value={expired_time}
|
||||
autoComplete='new-password'
|
||||
@ -205,7 +217,7 @@ const EditToken = () => {
|
||||
setExpiredTime(0, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
永不过期
|
||||
{t('token.edit.buttons.never_expire')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'button'}
|
||||
@ -213,7 +225,7 @@ const EditToken = () => {
|
||||
setExpiredTime(1, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
一个月后过期
|
||||
{t('token.edit.buttons.expire_1_month')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'button'}
|
||||
@ -221,7 +233,7 @@ const EditToken = () => {
|
||||
setExpiredTime(0, 1, 0, 0);
|
||||
}}
|
||||
>
|
||||
一天后过期
|
||||
{t('token.edit.buttons.expire_1_day')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'button'}
|
||||
@ -229,7 +241,7 @@ const EditToken = () => {
|
||||
setExpiredTime(0, 0, 1, 0);
|
||||
}}
|
||||
>
|
||||
一小时后过期
|
||||
{t('token.edit.buttons.expire_1_hour')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'button'}
|
||||
@ -237,17 +249,15 @@ const EditToken = () => {
|
||||
setExpiredTime(0, 0, 0, 1);
|
||||
}}
|
||||
>
|
||||
一分钟后过期
|
||||
{t('token.edit.buttons.expire_1_minute')}
|
||||
</Button>
|
||||
</div>
|
||||
<Message>
|
||||
注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。
|
||||
</Message>
|
||||
<Message>{t('token.edit.quota_notice')}</Message>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label={`额度${renderQuotaWithPrompt(remain_quota)}`}
|
||||
label={`${t('token.edit.quota')}${renderQuotaWithPrompt(remain_quota, t)}`}
|
||||
name='remain_quota'
|
||||
placeholder={'请输入额度'}
|
||||
placeholder={t('token.edit.quota_placeholder')}
|
||||
onChange={handleInputChange}
|
||||
value={remain_quota}
|
||||
autoComplete='new-password'
|
||||
@ -261,13 +271,15 @@ const EditToken = () => {
|
||||
setUnlimitedQuota();
|
||||
}}
|
||||
>
|
||||
{unlimited_quota ? '取消无限额度' : '设为无限额度'}
|
||||
{unlimited_quota
|
||||
? t('token.edit.buttons.cancel_unlimited')
|
||||
: t('token.edit.buttons.unlimited_quota')}
|
||||
</Button>
|
||||
<Button floated='right' positive onClick={submit}>
|
||||
提交
|
||||
{t('token.edit.buttons.submit')}
|
||||
</Button>
|
||||
<Button floated='right' onClick={handleCancel}>
|
||||
取消
|
||||
{t('token.edit.buttons.cancel')}
|
||||
</Button>
|
||||
</Form>
|
||||
</Card.Content>
|
||||
|
@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'semantic-ui-react';
|
||||
import TokensTable from '../../components/TokensTable';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Token = () => (
|
||||
<div className='dashboard-container'>
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header className='header'>令牌管理</Card.Header>
|
||||
<TokensTable />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
const Token = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='dashboard-container'>
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header className='header'>{t('token.title')}</Card.Header>
|
||||
<TokensTable />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Token;
|
||||
|
Loading…
Reference in New Issue
Block a user