mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-06 00:33:43 +08:00
feat: i18n for token related pages
This commit is contained in:
@@ -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": "$"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "$"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('无法正常连接至服务器!');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -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 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user