feat: i18n for token related pages

This commit is contained in:
JustSong
2025-02-01 15:11:07 +08:00
parent 93ce6c4cd7
commit 60f2776795
7 changed files with 385 additions and 185 deletions

View File

@@ -152,5 +152,93 @@
"tencent": "Enter in format: AppId|SecretId|SecretKey" "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": "$"
}
} }
} }

View File

@@ -152,5 +152,93 @@
"tencent": "按照如下格式输入AppId|SecretId|SecretKey" "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": "$"
}
} }
} }

View File

@@ -42,32 +42,36 @@ function App() {
} }
}; };
const loadStatus = async () => { const loadStatus = async () => {
const res = await API.get('/api/status'); try {
const { success, data } = res.data; const res = await API.get('/api/status');
if (success) { const { success, message, data } = res.data || {}; // Add default empty object
localStorage.setItem('status', JSON.stringify(data)); if (success && data) { // Check data exists
statusDispatch({ type: 'set', payload: data }); localStorage.setItem('status', JSON.stringify(data));
localStorage.setItem('system_name', data.system_name); statusDispatch({ type: 'set', payload: data });
localStorage.setItem('logo', data.logo); localStorage.setItem('system_name', data.system_name);
localStorage.setItem('footer_html', data.footer_html); localStorage.setItem('logo', data.logo);
localStorage.setItem('quota_per_unit', data.quota_per_unit); localStorage.setItem('footer_html', data.footer_html);
localStorage.setItem('display_in_currency', data.display_in_currency); localStorage.setItem('quota_per_unit', data.quota_per_unit);
if (data.chat_link) { localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('chat_link', data.chat_link); 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 { } else {
localStorage.removeItem('chat_link'); showError(message || '无法正常连接至服务器!');
} }
if ( } catch (error) {
data.version !== process.env.REACT_APP_VERSION && showError(error.message || '无法正常连接至服务器!');
data.version !== 'v0.0.0' &&
process.env.REACT_APP_VERSION !== ''
) {
showNotice(
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
);
}
} else {
showError('无法正常连接至服务器!');
} }
}; };

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
Button, Button,
Dropdown, Dropdown,
@@ -21,64 +22,63 @@ import {
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; 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) { function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>; return <>{timestamp2string(timestamp)}</>;
} }
function renderStatus(status) { function renderStatus(status, t) {
switch (status) { switch (status) {
case 1: case 1:
return ( return (
<Label basic color='green'> <Label basic color='green'>
已启用 {t('token.table.status_enabled')}
</Label> </Label>
); );
case 2: case 2:
return ( return (
<Label basic color='red'> <Label basic color='red'>
{' '} {t('token.table.status_disabled')}
已禁用{' '}
</Label> </Label>
); );
case 3: case 3:
return ( return (
<Label basic color='yellow'> <Label basic color='yellow'>
{' '} {t('token.table.status_expired')}
已过期{' '}
</Label> </Label>
); );
case 4: case 4:
return ( return (
<Label basic color='grey'> <Label basic color='grey'>
{' '} {t('token.table.status_depleted')}
已耗尽{' '}
</Label> </Label>
); );
default: default:
return ( return (
<Label basic color='black'> <Label basic color='black'>
{' '} {t('token.table.status_unknown')}
未知状态{' '}
</Label> </Label>
); );
} }
} }
const TokensTable = () => { 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 [tokens, setTokens] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1); const [activePage, setActivePage] = useState(1);
@@ -135,8 +135,7 @@ const TokensTable = () => {
let nextUrl; let nextUrl;
if (nextLink) { if (nextLink) {
nextUrl = nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
} else { } else {
nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
} }
@@ -153,17 +152,15 @@ const TokensTable = () => {
url = nextUrl; url = nextUrl;
break; break;
case 'lobechat': case 'lobechat':
url = url = nextLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
nextLink +
`/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
break; break;
default: default:
url = `sk-${key}`; url = `sk-${key}`;
} }
if (await copy(url)) { if (await copy(url)) {
showSuccess('已复制到剪贴板!'); showSuccess(t('token.messages.copy_success'));
} else { } else {
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); showWarning(t('token.messages.copy_failed'));
setSearchKeyword(url); setSearchKeyword(url);
} }
}; };
@@ -237,7 +234,7 @@ const TokensTable = () => {
} }
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess('操作成功完成!'); showSuccess(t('token.messages.operation_success'));
let token = res.data.data; let token = res.data.data;
let newTokens = [...tokens]; let newTokens = [...tokens];
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
@@ -308,7 +305,7 @@ const TokensTable = () => {
icon='search' icon='search'
fluid fluid
iconPosition='left' iconPosition='left'
placeholder='搜索令牌的名称 ...' placeholder={t('token.search')}
value={searchKeyword} value={searchKeyword}
loading={searching} loading={searching}
onChange={handleKeywordChange} onChange={handleKeywordChange}
@@ -324,7 +321,7 @@ const TokensTable = () => {
sortToken('name'); sortToken('name');
}} }}
> >
名称 {t('token.table.name')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
@@ -332,7 +329,7 @@ const TokensTable = () => {
sortToken('status'); sortToken('status');
}} }}
> >
状态 {t('token.table.status')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
@@ -340,7 +337,7 @@ const TokensTable = () => {
sortToken('used_quota'); sortToken('used_quota');
}} }}
> >
已用额度 {t('token.table.used_quota')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
@@ -348,7 +345,7 @@ const TokensTable = () => {
sortToken('remain_quota'); sortToken('remain_quota');
}} }}
> >
剩余额度 {t('token.table.remain_quota')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
@@ -356,7 +353,7 @@ const TokensTable = () => {
sortToken('created_time'); sortToken('created_time');
}} }}
> >
创建时间 {t('token.table.created_time')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
@@ -364,9 +361,9 @@ const TokensTable = () => {
sortToken('expired_time'); sortToken('expired_time');
}} }}
> >
过期时间 {t('token.table.expired_time')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell>操作</Table.HeaderCell> <Table.HeaderCell>{t('token.table.actions')}</Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
@@ -378,20 +375,37 @@ const TokensTable = () => {
) )
.map((token, idx) => { .map((token, idx) => {
if (token.deleted) return <></>; 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 ( return (
<Table.Row key={token.id}> <Table.Row key={token.id}>
<Table.Cell>{token.name ? token.name : '无'}</Table.Cell> <Table.Cell>
<Table.Cell>{renderStatus(token.status)}</Table.Cell> {token.name ? token.name : t('token.table.no_name')}
<Table.Cell>{renderQuota(token.used_quota)}</Table.Cell> </Table.Cell>
<Table.Cell>{renderStatus(token.status, t)}</Table.Cell>
<Table.Cell>{renderQuota(token.used_quota, t)}</Table.Cell>
<Table.Cell> <Table.Cell>
{token.unlimited_quota {token.unlimited_quota
? '无限制' ? t('token.table.unlimited')
: renderQuota(token.remain_quota, 2)} : renderQuota(token.remain_quota, t, 2)}
</Table.Cell> </Table.Cell>
<Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell> <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
<Table.Cell> <Table.Cell>
{token.expired_time === -1 {token.expired_time === -1
? '永不过期' ? t('token.table.never_expire')
: renderTimestamp(token.expired_time)} : renderTimestamp(token.expired_time)}
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>
@@ -400,21 +414,14 @@ const TokensTable = () => {
<Button <Button
size={'small'} size={'small'}
positive positive
onClick={async () => { onClick={async () => await onCopy('', token.key)}
await onCopy('', token.key);
}}
> >
复制 {t('token.buttons.copy')}
</Button> </Button>
<Dropdown <Dropdown
className='button icon' className='button icon'
floating floating
options={COPY_OPTIONS.map((option) => ({ options={copyOptionsWithHandlers}
...option,
onClick: async () => {
await onCopy(option.value, token.key);
},
}))}
trigger={<></>} trigger={<></>}
/> />
</Button.Group>{' '} </Button.Group>{' '}
@@ -422,28 +429,21 @@ const TokensTable = () => {
<Button <Button
size={'small'} size={'small'}
positive positive
onClick={() => { onClick={() => onOpenLink('', token.key)}
onOpenLink('', token.key);
}}
> >
聊天 {t('token.buttons.chat')}
</Button> </Button>
<Dropdown <Dropdown
className='button icon' className='button icon'
floating floating
options={OPEN_LINK_OPTIONS.map((option) => ({ options={openLinkOptionsWithHandlers}
...option,
onClick: async () => {
await onOpenLink(option.value, token.key);
},
}))}
trigger={<></>} trigger={<></>}
/> />
</Button.Group>{' '} </Button.Group>{' '}
<Popup <Popup
trigger={ trigger={
<Button size='small' negative> <Button size='small' negative>
删除 {t('token.buttons.delete')}
</Button> </Button>
} }
on='click' on='click'
@@ -456,7 +456,7 @@ const TokensTable = () => {
manageToken(token.id, 'delete', idx); manageToken(token.id, 'delete', idx);
}} }}
> >
删除令牌 {token.name} {t('token.buttons.confirm_delete')} {token.name}
</Button> </Button>
</Popup> </Popup>
<Button <Button
@@ -469,14 +469,12 @@ const TokensTable = () => {
); );
}} }}
> >
{token.status === 1 ? '禁用' : '启用'} {token.status === 1
? t('token.buttons.disable')
: t('token.buttons.enable')}
</Button> </Button>
<Button <Button size={'small'} as={Link} to={'/token/edit/' + token.id}>
size={'small'} {t('token.buttons.edit')}
as={Link}
to={'/token/edit/' + token.id}
>
编辑
</Button> </Button>
</div> </div>
</Table.Cell> </Table.Cell>
@@ -489,24 +487,24 @@ const TokensTable = () => {
<Table.Row> <Table.Row>
<Table.HeaderCell colSpan='7'> <Table.HeaderCell colSpan='7'>
<Button size='small' as={Link} to='/token/add' loading={loading}> <Button size='small' as={Link} to='/token/add' loading={loading}>
添加新的令牌 {t('token.buttons.add')}
</Button> </Button>
<Button size='small' onClick={refresh} loading={loading}> <Button size='small' onClick={refresh} loading={loading}>
刷新 {t('token.buttons.refresh')}
</Button> </Button>
<Dropdown <Dropdown
placeholder='排序方式' placeholder={t('token.sort.placeholder')}
selection selection
options={[ options={[
{ key: '', text: '默认排序', value: '' }, { key: '', text: t('token.sort.default'), value: '' },
{ {
key: 'remain_quota', key: 'remain_quota',
text: '按剩余额度排序', text: t('token.sort.by_remain'),
value: 'remain_quota', value: 'remain_quota',
}, },
{ {
key: 'used_quota', key: 'used_quota',
text: '按已用额度排序', text: t('token.sort.by_used'),
value: 'used_quota', value: 'used_quota',
}, },
]} ]}

View File

@@ -1,4 +1,5 @@
import { Label } from 'semantic-ui-react'; import { Label } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
export function renderText(text, limit) { export function renderText(text, limit) {
if (text.length > limit) { if (text.length > limit) {
@@ -39,23 +40,27 @@ export function renderNumber(num) {
} }
} }
export function renderQuota(quota, digits = 2) { export function renderQuota(quota, t, precision = 2) {
let quotaPerUnit = localStorage.getItem('quota_per_unit'); const displayInCurrency = localStorage.getItem('display_in_currency') === 'true';
let displayInCurrency = localStorage.getItem('display_in_currency'); const quotaPerUnit = parseFloat(localStorage.getItem('quota_per_unit') || '1');
quotaPerUnit = parseFloat(quotaPerUnit);
displayInCurrency = displayInCurrency === 'true';
if (displayInCurrency) { if (displayInCurrency) {
return '$' + (quota / quotaPerUnit).toFixed(digits); const amount = (quota / quotaPerUnit).toFixed(precision);
return t('common.quota.display_short', { amount });
} }
return renderNumber(quota); return renderNumber(quota);
} }
export function renderQuotaWithPrompt(quota, digits) { export function renderQuotaWithPrompt(quota, t) {
let displayInCurrency = localStorage.getItem('display_in_currency'); const displayInCurrency = localStorage.getItem('display_in_currency') === 'true';
displayInCurrency = displayInCurrency === 'true'; const quotaPerUnit = parseFloat(localStorage.getItem('quota_per_unit') || '1');
if (displayInCurrency) { if (displayInCurrency) {
return `(等价金额:${renderQuota(quota, digits)}`; const amount = (quota / quotaPerUnit).toFixed(2);
return ` (${t('common.quota.display', { amount })})`;
} }
return ''; return '';
} }

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
Button, Button,
Form, Form,
@@ -18,6 +19,7 @@ import {
import { renderQuotaWithPrompt } from '../../helpers/render'; import { renderQuotaWithPrompt } from '../../helpers/render';
const EditToken = () => { const EditToken = () => {
const { t } = useTranslation();
const params = useParams(); const params = useParams();
const tokenId = params.id; const tokenId = params.id;
const isEdit = tokenId !== undefined; const isEdit = tokenId !== undefined;
@@ -60,47 +62,61 @@ const EditToken = () => {
}; };
const loadToken = async () => { const loadToken = async () => {
let res = await API.get(`/api/token/${tokenId}`); try {
const { success, message, data } = res.data; let res = await API.get(`/api/token/${tokenId}`);
if (success) { const { success, message, data } = res.data || {};
if (data.expired_time !== -1) { if (success && data) {
data.expired_time = timestamp2string(data.expired_time); if (data.expired_time !== -1) {
} data.expired_time = timestamp2string(data.expired_time);
if (data.models === '') { }
data.models = []; if (data.models === '') {
data.models = [];
} else {
data.models = data.models.split(',');
}
setInputs(data);
} else { } else {
data.models = data.models.split(','); showError(message || 'Failed to load token');
} }
setInputs(data); } catch (error) {
} else { showError(error.message || 'Network error');
showError(message);
} }
setLoading(false); setLoading(false);
}; };
useEffect(() => {
if (isEdit) {
loadToken().then();
}
loadAvailableModels().then();
}, []);
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
let res = await API.get(`/api/user/available_models`); try {
const { success, message, data } = res.data; let res = await API.get(`/api/user/available_models`);
if (success) { const { success, message, data } = res.data || {};
let options = data.map((model) => { if (success && data) {
return { let options = data.map((model) => {
key: model, return {
text: model, key: model,
value: model, text: model,
}; value: model,
}); };
setModelOptions(options); });
} else { setModelOptions(options);
showError(message); } 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 () => { const submit = async () => {
if (!isEdit && inputs.name === '') return; if (!isEdit && inputs.name === '') return;
let localInputs = inputs; let localInputs = inputs;
@@ -108,7 +124,7 @@ const EditToken = () => {
if (localInputs.expired_time !== -1) { if (localInputs.expired_time !== -1) {
let time = Date.parse(localInputs.expired_time); let time = Date.parse(localInputs.expired_time);
if (isNaN(time)) { if (isNaN(time)) {
showError('过期时间格式错误!'); showError(t('token.edit.messages.expire_time_invalid'));
return; return;
} }
localInputs.expired_time = Math.ceil(time / 1000); localInputs.expired_time = Math.ceil(time / 1000);
@@ -126,9 +142,9 @@ const EditToken = () => {
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
if (isEdit) { if (isEdit) {
showSuccess('令牌更新成功!'); showSuccess(t('token.edit.messages.update_success'));
} else { } else {
showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!'); showSuccess(t('token.edit.messages.create_success'));
setInputs(originInputs); setInputs(originInputs);
} }
} else { } else {
@@ -141,14 +157,14 @@ const EditToken = () => {
<Card fluid className='chart-card'> <Card fluid className='chart-card'>
<Card.Content> <Card.Content>
<Card.Header className='header'> <Card.Header className='header'>
{isEdit ? '更新令牌信息' : '创建新的令牌'} {isEdit ? t('token.edit.title_edit') : t('token.edit.title_create')}
</Card.Header> </Card.Header>
<Form loading={loading} autoComplete='new-password'> <Form loading={loading} autoComplete='new-password'>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='名称' label={t('token.edit.name')}
name='name' name='name'
placeholder={'请输入名称'} placeholder={t('token.edit.name_placeholder')}
onChange={handleInputChange} onChange={handleInputChange}
value={name} value={name}
autoComplete='new-password' autoComplete='new-password'
@@ -157,8 +173,8 @@ const EditToken = () => {
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Dropdown <Form.Dropdown
label='模型范围' label={t('token.edit.models')}
placeholder={'请选择允许使用的模型,留空则不进行限制'} placeholder={t('token.edit.models_placeholder')}
name='models' name='models'
fluid fluid
multiple multiple
@@ -175,11 +191,9 @@ const EditToken = () => {
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='IP 限制' label={t('token.edit.ip_limit')}
name='subnet' name='subnet'
placeholder={ placeholder={t('token.edit.ip_limit_placeholder')}
'请输入允许访问的网段例如192.168.0.0/24请使用英文逗号分隔多个网段'
}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.subnet} value={inputs.subnet}
autoComplete='new-password' autoComplete='new-password'
@@ -187,11 +201,9 @@ const EditToken = () => {
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='过期时间' label={t('token.edit.expire_time')}
name='expired_time' name='expired_time'
placeholder={ placeholder={t('token.edit.expire_time_placeholder')}
'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss-1 表示无限制'
}
onChange={handleInputChange} onChange={handleInputChange}
value={expired_time} value={expired_time}
autoComplete='new-password' autoComplete='new-password'
@@ -205,7 +217,7 @@ const EditToken = () => {
setExpiredTime(0, 0, 0, 0); setExpiredTime(0, 0, 0, 0);
}} }}
> >
永不过期 {t('token.edit.buttons.never_expire')}
</Button> </Button>
<Button <Button
type={'button'} type={'button'}
@@ -213,7 +225,7 @@ const EditToken = () => {
setExpiredTime(1, 0, 0, 0); setExpiredTime(1, 0, 0, 0);
}} }}
> >
一个月后过期 {t('token.edit.buttons.expire_1_month')}
</Button> </Button>
<Button <Button
type={'button'} type={'button'}
@@ -221,7 +233,7 @@ const EditToken = () => {
setExpiredTime(0, 1, 0, 0); setExpiredTime(0, 1, 0, 0);
}} }}
> >
一天后过期 {t('token.edit.buttons.expire_1_day')}
</Button> </Button>
<Button <Button
type={'button'} type={'button'}
@@ -229,7 +241,7 @@ const EditToken = () => {
setExpiredTime(0, 0, 1, 0); setExpiredTime(0, 0, 1, 0);
}} }}
> >
一小时后过期 {t('token.edit.buttons.expire_1_hour')}
</Button> </Button>
<Button <Button
type={'button'} type={'button'}
@@ -237,17 +249,15 @@ const EditToken = () => {
setExpiredTime(0, 0, 0, 1); setExpiredTime(0, 0, 0, 1);
}} }}
> >
一分钟后过期 {t('token.edit.buttons.expire_1_minute')}
</Button> </Button>
</div> </div>
<Message> <Message>{t('token.edit.quota_notice')}</Message>
注意令牌的额度仅用于限制令牌本身的最大额度使用量实际的使用受到账户的剩余额度限制
</Message>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label={`额度${renderQuotaWithPrompt(remain_quota)}`} label={`${t('token.edit.quota')}${renderQuotaWithPrompt(remain_quota, t)}`}
name='remain_quota' name='remain_quota'
placeholder={'请输入额度'} placeholder={t('token.edit.quota_placeholder')}
onChange={handleInputChange} onChange={handleInputChange}
value={remain_quota} value={remain_quota}
autoComplete='new-password' autoComplete='new-password'
@@ -261,13 +271,15 @@ const EditToken = () => {
setUnlimitedQuota(); setUnlimitedQuota();
}} }}
> >
{unlimited_quota ? '取消无限额度' : '设为无限额度'} {unlimited_quota
? t('token.edit.buttons.cancel_unlimited')
: t('token.edit.buttons.unlimited_quota')}
</Button> </Button>
<Button floated='right' positive onClick={submit}> <Button floated='right' positive onClick={submit}>
提交 {t('token.edit.buttons.submit')}
</Button> </Button>
<Button floated='right' onClick={handleCancel}> <Button floated='right' onClick={handleCancel}>
取消 {t('token.edit.buttons.cancel')}
</Button> </Button>
</Form> </Form>
</Card.Content> </Card.Content>

View File

@@ -1,16 +1,21 @@
import React from 'react'; import React from 'react';
import { Card } from 'semantic-ui-react'; import { Card } from 'semantic-ui-react';
import TokensTable from '../../components/TokensTable'; import TokensTable from '../../components/TokensTable';
import { useTranslation } from 'react-i18next';
const Token = () => ( const Token = () => {
<div className='dashboard-container'> const { t } = useTranslation();
<Card fluid className='chart-card'>
<Card.Content> return (
<Card.Header className='header'>令牌管理</Card.Header> <div className='dashboard-container'>
<TokensTable /> <Card fluid className='chart-card'>
</Card.Content> <Card.Content>
</Card> <Card.Header className='header'>{t('token.title')}</Card.Header>
</div> <TokensTable />
); </Card.Content>
</Card>
</div>
);
};
export default Token; export default Token;