From 60f2776795d680650d0aa087ef62314457f9edc0 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 15:11:07 +0800 Subject: [PATCH 01/57] feat: i18n for token related pages --- .../public/locales/en/translation.json | 88 ++++++++++ .../public/locales/zh/translation.json | 88 ++++++++++ web/default/src/App.js | 52 +++--- web/default/src/components/TokensTable.js | 160 +++++++++--------- web/default/src/helpers/render.js | 25 +-- web/default/src/pages/Token/EditToken.js | 132 ++++++++------- web/default/src/pages/Token/index.js | 25 +-- 7 files changed, 385 insertions(+), 185 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index baab810b..3c1b23fa 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -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": "$" + } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 8b53e005..9cf78f34 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -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": "$" + } } } diff --git a/web/default/src/App.js b/web/default/src/App.js index 3597f71b..0e63a403 100644 --- a/web/default/src/App.js +++ b/web/default/src/App.js @@ -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 || '无法正常连接至服务器!'); } }; diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 4fee9773..401d5fe8 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -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 ( ); case 2: return ( ); case 3: return ( ); case 4: return ( ); default: return ( ); } } 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')} { sortToken('status'); }} > - 状态 + {t('token.table.status')} { sortToken('used_quota'); }} > - 已用额度 + {t('token.table.used_quota')} { sortToken('remain_quota'); }} > - 剩余额度 + {t('token.table.remain_quota')} { sortToken('created_time'); }} > - 创建时间 + {t('token.table.created_time')} { sortToken('expired_time'); }} > - 过期时间 + {t('token.table.expired_time')} - 操作 + {t('token.table.actions')} @@ -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 ( - {token.name ? token.name : '无'} - {renderStatus(token.status)} - {renderQuota(token.used_quota)} + + {token.name ? token.name : t('token.table.no_name')} + + {renderStatus(token.status, t)} + {renderQuota(token.used_quota, t)} {token.unlimited_quota - ? '无限制' - : renderQuota(token.remain_quota, 2)} + ? t('token.table.unlimited') + : renderQuota(token.remain_quota, t, 2)} {renderTimestamp(token.created_time)} {token.expired_time === -1 - ? '永不过期' + ? t('token.table.never_expire') : renderTimestamp(token.expired_time)} @@ -400,21 +414,14 @@ const TokensTable = () => { ({ - ...option, - onClick: async () => { - await onCopy(option.value, token.key); - }, - }))} + options={copyOptionsWithHandlers} trigger={<>} /> {' '} @@ -422,28 +429,21 @@ const TokensTable = () => { ({ - ...option, - onClick: async () => { - await onOpenLink(option.value, token.key); - }, - }))} + options={openLinkOptionsWithHandlers} trigger={<>} /> {' '} - 删除 + {t('token.buttons.delete')} } on='click' @@ -456,7 +456,7 @@ const TokensTable = () => { manageToken(token.id, 'delete', idx); }} > - 删除令牌 {token.name} + {t('token.buttons.confirm_delete')} {token.name} - @@ -489,24 +487,24 @@ const TokensTable = () => { 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 ''; } diff --git a/web/default/src/pages/Token/EditToken.js b/web/default/src/pages/Token/EditToken.js index 3e7517f8..28ae1e59 100644 --- a/web/default/src/pages/Token/EditToken.js +++ b/web/default/src/pages/Token/EditToken.js @@ -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 = () => { - {isEdit ? '更新令牌信息' : '创建新的令牌'} + {isEdit ? t('token.edit.title_edit') : t('token.edit.title_create')}
{ { { { setExpiredTime(0, 0, 0, 0); }} > - 永不过期 + {t('token.edit.buttons.never_expire')} - - 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 - + {t('token.edit.quota_notice')} { setUnlimitedQuota(); }} > - {unlimited_quota ? '取消无限额度' : '设为无限额度'} + {unlimited_quota + ? t('token.edit.buttons.cancel_unlimited') + : t('token.edit.buttons.unlimited_quota')}
diff --git a/web/default/src/pages/Token/index.js b/web/default/src/pages/Token/index.js index 8745e0af..19ef58e8 100644 --- a/web/default/src/pages/Token/index.js +++ b/web/default/src/pages/Token/index.js @@ -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 = () => ( -
- - - 令牌管理 - - - -
-); +const Token = () => { + const { t } = useTranslation(); + + return ( +
+ + + {t('token.title')} + + + +
+ ); +}; export default Token; From ae20aea5553890f4deaab4c5cfc0dc017a5d1501 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 17:00:24 +0800 Subject: [PATCH 02/57] feat: i18n support --- .../public/locales/en/translation.json | 31 ++++++++++++++++ .../public/locales/zh/translation.json | 31 ++++++++++++++++ web/default/src/App.js | 5 +-- web/default/src/components/LogsTable.js | 10 +++--- .../src/components/RedemptionsTable.js | 19 +++++----- web/default/src/components/TokensTable.js | 35 ++++++++++++------- web/default/src/components/UsersTable.js | 6 ++-- web/default/src/helpers/render.js | 22 +++++++----- web/default/src/pages/Log/index.js | 25 +++++++------ .../src/pages/Redemption/EditRedemption.js | 22 +++++++----- web/default/src/pages/Token/EditToken.js | 9 +++-- web/default/src/pages/Token/index.js | 2 +- web/default/src/pages/TopUp/index.js | 2 +- 13 files changed, 156 insertions(+), 63 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index 3c1b23fa..ea32c04c 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -240,5 +240,36 @@ "display_short": "${{amount}}", "unit": "$" } + }, + "redemption": { + "title": "Redemption Management", + "edit": { + "title_edit": "Update Redemption Code", + "title_create": "Create New Redemption Code", + "name": "Name", + "name_placeholder": "Please enter name", + "quota": "Quota", + "quota_placeholder": "Please enter quota per redemption code", + "count": "Generate Count", + "count_placeholder": "Please enter number of codes to generate", + "buttons": { + "submit": "Submit", + "cancel": "Cancel" + } + } + }, + "log": { + "title": "Operation Log", + "usage_details": "Usage Details", + "total_quota": "Total Quota Used", + "click_to_view": "Click to View", + "table": { + "id": "ID", + "username": "Username", + "type": "Type", + "content": "Content", + "amount": "Amount", + "time": "Time" + } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 9cf78f34..9a978289 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -240,5 +240,36 @@ "display_short": "${{amount}}", "unit": "$" } + }, + "redemption": { + "title": "兑换管理", + "edit": { + "title_edit": "更新兑换码信息", + "title_create": "创建新的兑换码", + "name": "名称", + "name_placeholder": "请输入名称", + "quota": "额度", + "quota_placeholder": "请输入单个兑换码中包含的额度", + "count": "生成数量", + "count_placeholder": "请输入生成数量", + "buttons": { + "submit": "提交", + "cancel": "取消" + } + } + }, + "log": { + "title": "操作日志", + "usage_details": "使用明细", + "total_quota": "总消耗额度", + "click_to_view": "点击查看", + "table": { + "id": "ID", + "username": "用户名", + "type": "类型", + "content": "内容", + "amount": "数量", + "time": "时间" + } } } diff --git a/web/default/src/App.js b/web/default/src/App.js index 0e63a403..e76ee7ea 100644 --- a/web/default/src/App.js +++ b/web/default/src/App.js @@ -44,8 +44,9 @@ function App() { const loadStatus = async () => { try { const res = await API.get('/api/status'); - const { success, message, data } = res.data || {}; // Add default empty object - if (success && data) { // Check data exists + 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); diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index e1c13331..5cf7ceba 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -18,6 +18,7 @@ import { showWarning, timestamp2string, } from '../helpers'; +import { useTranslation } from 'react-i18next'; import { ITEMS_PER_PAGE } from '../constants'; import { renderColorLabel, renderQuota } from '../helpers/render'; @@ -137,6 +138,7 @@ function renderDetail(log) { } const LogsTable = () => { + const { t } = useTranslation(); const [logs, setLogs] = useState([]); const [showStat, setShowStat] = useState(false); const [loading, setLoading] = useState(true); @@ -309,14 +311,14 @@ const LogsTable = () => { <> <>
- 使用明细(总消耗额度: - {showStat && renderQuota(stat.quota)} + {t('log.usage_details')}({t('log.total_quota')}: + {showStat && renderQuota(stat.quota, t)} {!showStat && ( - 点击查看 + {t('log.click_to_view')} )} ) @@ -554,7 +556,7 @@ const LogsTable = () => { {log.completion_tokens ? log.completion_tokens : ''} - {log.quota ? renderQuota(log.quota, 6) : ''} + {log.quota ? renderQuota(log.quota, t, 6) : ''} )} diff --git a/web/default/src/components/RedemptionsTable.js b/web/default/src/components/RedemptionsTable.js index ccea9ff3..70dab123 100644 --- a/web/default/src/components/RedemptionsTable.js +++ b/web/default/src/components/RedemptionsTable.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, @@ -25,39 +26,37 @@ function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; } -function renderStatus(status) { +function renderStatus(status, t) { switch (status) { case 1: return ( ); case 2: return ( ); case 3: return ( ); default: return ( ); } } const RedemptionsTable = () => { + const { t } = useTranslation(); const [redemptions, setRedemptions] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); @@ -260,8 +259,8 @@ const RedemptionsTable = () => { {redemption.name ? redemption.name : '无'} - {renderStatus(redemption.status)} - {renderQuota(redemption.quota)} + {renderStatus(redemption.status, t)} + {renderQuota(redemption.quota, t)} {renderTimestamp(redemption.created_time)} diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 401d5fe8..f8e9bcdb 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -69,14 +69,14 @@ const TokensTable = () => { { 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' } + { 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' } + { key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' }, ]; const [tokens, setTokens] = useState([]); @@ -135,7 +135,8 @@ 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}"}`; } @@ -152,7 +153,9 @@ 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}`; @@ -376,19 +379,21 @@ const TokensTable = () => { .map((token, idx) => { if (token.deleted) return <>; - const copyOptionsWithHandlers = COPY_OPTIONS.map(option => ({ + 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); - } - })); + const openLinkOptionsWithHandlers = OPEN_LINK_OPTIONS.map( + (option) => ({ + ...option, + onClick: async () => { + await onOpenLink(option.value, token.key); + }, + }) + ); return ( @@ -473,7 +478,11 @@ const TokensTable = () => { ? t('token.buttons.disable') : t('token.buttons.enable')} - diff --git a/web/default/src/components/UsersTable.js b/web/default/src/components/UsersTable.js index 2e08b715..4951d104 100644 --- a/web/default/src/components/UsersTable.js +++ b/web/default/src/components/UsersTable.js @@ -10,6 +10,7 @@ import { } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; import { API, showError, showSuccess } from '../helpers'; +import { useTranslation } from 'react-i18next'; import { ITEMS_PER_PAGE } from '../constants'; import { @@ -33,6 +34,7 @@ function renderRole(role) { } const UsersTable = () => { + const { t } = useTranslation(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); @@ -266,12 +268,12 @@ const UsersTable = () => { {renderQuota(user.quota)}} + trigger={} /> {renderQuota(user.used_quota)} + } /> ( -
- - - {/*操作日志*/} - - - -
-); +const Log = () => { + const { t } = useTranslation(); + + return ( +
+ + + {t('log.title')} + + + +
+ ); +}; export default Log; diff --git a/web/default/src/pages/Redemption/EditRedemption.js b/web/default/src/pages/Redemption/EditRedemption.js index e0801830..1aee6e10 100644 --- a/web/default/src/pages/Redemption/EditRedemption.js +++ b/web/default/src/pages/Redemption/EditRedemption.js @@ -1,10 +1,12 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, Card } from 'semantic-ui-react'; import { useParams, useNavigate } from 'react-router-dom'; import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; const EditRedemption = () => { + const { t } = useTranslation(); const params = useParams(); const navigate = useNavigate(); const redemptionId = params.id; @@ -83,14 +85,14 @@ const EditRedemption = () => { - {isEdit ? '更新兑换码信息' : '创建新的兑换码'} + {isEdit ? t('redemption.edit.title_edit') : t('redemption.edit.title_create')}
{ { <> { )} + -
diff --git a/web/default/src/pages/Token/EditToken.js b/web/default/src/pages/Token/EditToken.js index 28ae1e59..e5bfe786 100644 --- a/web/default/src/pages/Token/EditToken.js +++ b/web/default/src/pages/Token/EditToken.js @@ -107,12 +107,12 @@ const EditToken = () => { useEffect(() => { if (isEdit) { - loadToken().catch(error => { + loadToken().catch((error) => { showError(error.message || 'Failed to load token'); setLoading(false); }); } - loadAvailableModels().catch(error => { + loadAvailableModels().catch((error) => { showError(error.message || 'Failed to load models'); }); }, []); @@ -255,7 +255,10 @@ const EditToken = () => { {t('token.edit.quota_notice')} { const { t } = useTranslation(); - + return (
diff --git a/web/default/src/pages/TopUp/index.js b/web/default/src/pages/TopUp/index.js index 9ffb6af0..2d037646 100644 --- a/web/default/src/pages/TopUp/index.js +++ b/web/default/src/pages/TopUp/index.js @@ -131,7 +131,7 @@ const TopUp = () => {
- {renderQuota(userQuota)} + {renderQuota(userQuota, t)} {t('topup.get_code.current_quota')} From 2c8c29bfc7a6e0f8d675fc9567e770613a266ba8 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 17:04:31 +0800 Subject: [PATCH 03/57] feat: i18n support --- .../public/locales/en/translation.json | 32 +++++++++++ .../public/locales/zh/translation.json | 32 +++++++++++ .../src/components/RedemptionsTable.js | 54 ++++++++++--------- .../src/pages/Redemption/EditRedemption.js | 4 +- web/default/src/pages/Redemption/index.js | 25 +++++---- 5 files changed, 110 insertions(+), 37 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index ea32c04c..034d1f4b 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -243,6 +243,34 @@ }, "redemption": { "title": "Redemption Management", + "search": "Search redemption codes by ID and name ...", + "table": { + "id": "ID", + "name": "Name", + "status": "Status", + "quota": "Quota", + "created_time": "Created Time", + "redeemed_time": "Redeemed Time", + "actions": "Actions", + "no_name": "None", + "not_redeemed": "Not Redeemed" + }, + "buttons": { + "copy": "Copy", + "delete": "Delete", + "confirm_delete": "Confirm Delete", + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "add": "Add New Code", + "refresh": "Refresh" + }, + "status": { + "unused": "Unused", + "disabled": "Disabled", + "used": "Used", + "unknown": "Unknown" + }, "edit": { "title_edit": "Update Redemption Code", "title_create": "Create New Redemption Code", @@ -256,6 +284,10 @@ "submit": "Submit", "cancel": "Cancel" } + }, + "messages": { + "update_success": "Redemption code updated successfully!", + "create_success": "Redemption code created successfully!" } }, "log": { diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 9a978289..e28cf678 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -243,6 +243,34 @@ }, "redemption": { "title": "兑换管理", + "search": "搜索兑换码的 ID 和名称 ...", + "table": { + "id": "ID", + "name": "名称", + "status": "状态", + "quota": "额度", + "created_time": "创建时间", + "redeemed_time": "兑换时间", + "actions": "操作", + "no_name": "无", + "not_redeemed": "尚未兑换" + }, + "buttons": { + "copy": "复制", + "delete": "删除", + "confirm_delete": "确认删除", + "enable": "启用", + "disable": "禁用", + "edit": "编辑", + "add": "添加新的兑换码", + "refresh": "刷新" + }, + "status": { + "unused": "未使用", + "disabled": "已禁用", + "used": "已使用", + "unknown": "未知状态" + }, "edit": { "title_edit": "更新兑换码信息", "title_create": "创建新的兑换码", @@ -256,6 +284,10 @@ "submit": "提交", "cancel": "取消" } + }, + "messages": { + "update_success": "兑换码更新成功!", + "create_success": "兑换码创建成功!" } }, "log": { diff --git a/web/default/src/components/RedemptionsTable.js b/web/default/src/components/RedemptionsTable.js index 70dab123..6f88ec67 100644 --- a/web/default/src/components/RedemptionsTable.js +++ b/web/default/src/components/RedemptionsTable.js @@ -176,6 +176,12 @@ const RedemptionsTable = () => { setLoading(false); }; + const refresh = async () => { + setLoading(true); + await loadRedemptions(0); + setActivePage(1); + }; + return ( <>
@@ -183,7 +189,7 @@ const RedemptionsTable = () => { icon='search' fluid iconPosition='left' - placeholder='搜索兑换码的 ID 和名称 ...' + placeholder={t('redemption.search')} value={searchKeyword} loading={searching} onChange={handleKeywordChange} @@ -199,7 +205,7 @@ const RedemptionsTable = () => { sortRedemption('id'); }} > - ID + {t('redemption.table.id')} { sortRedemption('name'); }} > - 名称 + {t('redemption.table.name')} { sortRedemption('status'); }} > - 状态 + {t('redemption.table.status')} { sortRedemption('quota'); }} > - 额度 + {t('redemption.table.quota')} { sortRedemption('created_time'); }} > - 创建时间 + {t('redemption.table.created_time')} { sortRedemption('redeemed_time'); }} > - 兑换时间 + {t('redemption.table.redeemed_time')} - 操作 + {t('redemption.table.actions')} @@ -276,21 +282,19 @@ const RedemptionsTable = () => { positive onClick={async () => { if (await copy(redemption.key)) { - showSuccess('已复制到剪贴板!'); + showSuccess(t('redemption.messages.copy_success')); } else { - showWarning( - '无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。' - ); + showWarning(t('redemption.messages.copy_failed')); setSearchKeyword(redemption.key); } }} > - 复制 + {t('redemption.buttons.copy')} - 删除 + {t('redemption.buttons.delete')} } on='click' @@ -303,7 +307,7 @@ const RedemptionsTable = () => { manageRedemption(redemption.id, 'delete', idx); }} > - 确认删除 + {t('redemption.buttons.confirm_delete')}
@@ -335,14 +341,12 @@ const RedemptionsTable = () => { - - + { const { success, message, data } = res.data; if (success) { if (isEdit) { - showSuccess('兑换码更新成功!'); + showSuccess(t('redemption.messages.update_success')); } else { - showSuccess('兑换码创建成功!'); + showSuccess(t('redemption.messages.create_success')); setInputs(originInputs); } } else { diff --git a/web/default/src/pages/Redemption/index.js b/web/default/src/pages/Redemption/index.js index 364a8c81..f07b0cd8 100644 --- a/web/default/src/pages/Redemption/index.js +++ b/web/default/src/pages/Redemption/index.js @@ -1,16 +1,21 @@ import React from 'react'; import { Card } from 'semantic-ui-react'; +import { useTranslation } from 'react-i18next'; import RedemptionsTable from '../../components/RedemptionsTable'; -const Redemption = () => ( -
- - - 兑换管理 - - - -
-); +const Redemption = () => { + const { t } = useTranslation(); + + return ( +
+ + + {t('redemption.title')} + + + +
+ ); +}; export default Redemption; From 6ca6a3ea744ac69f766517a70bd8dbaec90b8370 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 17:05:46 +0800 Subject: [PATCH 04/57] feat: i18n support --- web/default/src/components/RedemptionsTable.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/default/src/components/RedemptionsTable.js b/web/default/src/components/RedemptionsTable.js index 6f88ec67..76a4e91f 100644 --- a/web/default/src/components/RedemptionsTable.js +++ b/web/default/src/components/RedemptionsTable.js @@ -116,7 +116,7 @@ const RedemptionsTable = () => { } const { success, message } = res.data; if (success) { - showSuccess('操作成功完成!'); + showSuccess(t('token.messages.operation_success')); let redemption = res.data.data; let newRedemptions = [...redemptions]; let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; @@ -282,9 +282,9 @@ const RedemptionsTable = () => { positive onClick={async () => { if (await copy(redemption.key)) { - showSuccess(t('redemption.messages.copy_success')); + showSuccess(t('token.messages.copy_success')); } else { - showWarning(t('redemption.messages.copy_failed')); + showWarning(t('token.messages.copy_failed')); setSearchKeyword(redemption.key); } }} From 4a5f872dceb5c6de26a3704129055abb272d7d03 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 23:32:36 +0800 Subject: [PATCH 05/57] feat: i18n support --- web/default/public/locales/en/translation.json | 6 +++--- web/default/public/locales/zh/translation.json | 6 +++--- web/default/src/components/ChannelsTable.js | 17 ++++------------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index 034d1f4b..f3b42731 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -81,10 +81,10 @@ "hide_detail": "Hide Details" }, "messages": { - "test_success": "Channel ${name} test successful, model ${model}, time ${time}s, output: ${message}", + "test_success": "Channel {{name}} test successful, model {{model}}, time {{time}}s, output: {{message}}", "test_all_started": "Channel testing started successfully, please refresh page to see results.", - "delete_disabled_success": "Deleted all disabled channels, total: ${count}", - "balance_update_success": "Channel ${name} balance updated successfully!", + "delete_disabled_success": "Deleted all disabled channels, total: {{count}}", + "balance_update_success": "Channel {{name}} balance updated successfully!", "all_balance_updated": "All enabled channel balances have been updated!" }, "edit": { diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index e28cf678..3245b8a2 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -81,10 +81,10 @@ "hide_detail": "隐藏详情" }, "messages": { - "test_success": "渠道 ${name} 测试成功,模型 ${model},耗时 ${time} 秒,模型输出:${message}", + "test_success": "渠道 {{name}} 测试成功,模型 {{model}},耗时 {{time}} 秒,模型输出:{{message}}", "test_all_started": "已成功开始测试渠道,请刷新页面查看结果。", - "delete_disabled_success": "已删除所有禁用渠道,共计 ${count} 个", - "balance_update_success": "渠道 ${name} 余额更新成功!", + "delete_disabled_success": "已删除所有禁用渠道,共计 {{count}} 个", + "balance_update_success": "渠道 {{name}} 余额更新成功!", "all_balance_updated": "已更新完毕所有已启用渠道余额!" }, "edit": { diff --git a/web/default/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js index 51cefed8..d19be510 100644 --- a/web/default/src/components/ChannelsTable.js +++ b/web/default/src/components/ChannelsTable.js @@ -198,7 +198,7 @@ const ChannelsTable = () => { } const { success, message } = res.data; if (success) { - showSuccess('操作成功完成!'); + showSuccess(t('channel.messages.operation_success')); let channel = res.data.data; let newChannels = [...channels]; let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; @@ -325,14 +325,7 @@ const ChannelsTable = () => { newChannels[realIdx].response_time = time * 1000; newChannels[realIdx].test_time = Date.now() / 1000; setChannels(newChannels); - showInfo( - t('channel.messages.test_success', { - name: name, - model: model, - time: time.toFixed(2), - message: message, - }) - ); + showSuccess(t('channel.messages.test_success', { name, model, time, message })); } else { showError(message); } @@ -357,9 +350,7 @@ const ChannelsTable = () => { const res = await API.delete(`/api/channel/disabled`); const { success, message, data } = res.data; if (success) { - showSuccess( - t('channel.messages.delete_disabled_success', { count: data }) - ); + showSuccess(t('channel.messages.delete_disabled_success', { count: data })); await refresh(); } else { showError(message); @@ -375,7 +366,7 @@ const ChannelsTable = () => { newChannels[realIdx].balance = balance; newChannels[realIdx].balance_updated_time = Date.now() / 1000; setChannels(newChannels); - showInfo(t('channel.messages.balance_update_success', { name: name })); + showSuccess(t('channel.messages.balance_update_success', { name })); } else { showError(message); } From 958f2f4ea879fbfdfb6b2916c5b6bb93642fc90f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 23:42:00 +0800 Subject: [PATCH 06/57] feat: i18n support --- .../public/locales/en/translation.json | 35 +++++++++++++ .../public/locales/zh/translation.json | 35 +++++++++++++ web/default/src/pages/User/AddUser.js | 34 +++++++------ web/default/src/pages/User/EditUser.js | 51 ++++++++++--------- 4 files changed, 116 insertions(+), 39 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index f3b42731..0265b7b9 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -303,5 +303,40 @@ "amount": "Amount", "time": "Time" } + }, + "user": { + "edit": { + "title": "Update User Information", + "username": "Username", + "username_placeholder": "Please enter new username", + "password": "Password", + "password_placeholder": "Please enter new password, minimum 8 characters", + "display_name": "Display Name", + "display_name_placeholder": "Please enter new display name", + "group": "Group", + "group_placeholder": "Please select group", + "group_addition": "Please edit group multipliers in system settings to add new group:", + "quota": "Remaining Quota", + "quota_placeholder": "Please enter new remaining quota", + "github_id": "Linked GitHub Account", + "github_id_placeholder": "Read-only, user must link through personal settings page, cannot be modified directly", + "wechat_id": "Linked WeChat Account", + "wechat_id_placeholder": "Read-only, user must link through personal settings page, cannot be modified directly", + "email": "Linked Email Account", + "email_placeholder": "Read-only, user must link through personal settings page, cannot be modified directly", + "buttons": { + "submit": "Submit", + "cancel": "Cancel" + } + }, + "messages": { + "update_success": "User information updated successfully!" + }, + "add": { + "title": "Create New User Account" + }, + "messages": { + "create_success": "User account created successfully!" + } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 3245b8a2..ff1bdbb0 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -303,5 +303,40 @@ "amount": "数量", "time": "时间" } + }, + "user": { + "edit": { + "title": "更新用户信息", + "username": "用户名", + "username_placeholder": "请输入新的用户名", + "password": "密码", + "password_placeholder": "请输入新的密码,最短 8 位", + "display_name": "显示名称", + "display_name_placeholder": "请输入新的显示名称", + "group": "分组", + "group_placeholder": "请选择分组", + "group_addition": "请在系统设置页面编辑分组倍率以添加新的分组:", + "quota": "剩余额度", + "quota_placeholder": "请输入新的剩余额度", + "github_id": "已绑定的 GitHub 账户", + "github_id_placeholder": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改", + "wechat_id": "已绑定的微信账户", + "wechat_id_placeholder": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改", + "email": "已绑定的邮箱账户", + "email_placeholder": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改", + "buttons": { + "submit": "提交", + "cancel": "取消" + } + }, + "messages": { + "update_success": "用户信息更新成功!" + }, + "add": { + "title": "创建新用户账户" + }, + "messages": { + "create_success": "用户账户创建成功!" + } } } diff --git a/web/default/src/pages/User/AddUser.js b/web/default/src/pages/User/AddUser.js index f9f4bc18..c261ab4c 100644 --- a/web/default/src/pages/User/AddUser.js +++ b/web/default/src/pages/User/AddUser.js @@ -1,8 +1,10 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, Header, Segment } from 'semantic-ui-react'; import { API, showError, showSuccess } from '../../helpers'; const AddUser = () => { + const { t } = useTranslation(); const originInputs = { username: '', display_name: '', @@ -20,7 +22,7 @@ const AddUser = () => { const res = await API.post(`/api/user/`, inputs); const { success, message } = res.data; if (success) { - showSuccess('用户账户创建成功!'); + showSuccess(t('user.messages.create_success')); setInputs(originInputs); } else { showError(message); @@ -30,43 +32,43 @@ const AddUser = () => { return ( <> -
创建新用户账户
- +
{t('user.add.title')}
+
diff --git a/web/default/src/pages/User/EditUser.js b/web/default/src/pages/User/EditUser.js index f256b68e..8b154230 100644 --- a/web/default/src/pages/User/EditUser.js +++ b/web/default/src/pages/User/EditUser.js @@ -1,10 +1,12 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, Card } from 'semantic-ui-react'; import { useParams, useNavigate } from 'react-router-dom'; import { API, showError, showSuccess } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; const EditUser = () => { + const { t } = useTranslation(); const params = useParams(); const userId = params.id; const [loading, setLoading] = useState(true); @@ -86,7 +88,7 @@ const EditUser = () => { } const { success, message } = res.data; if (success) { - showSuccess('用户信息更新成功!'); + showSuccess(t('user.messages.update_success')); } else { showError(message); } @@ -96,13 +98,13 @@ const EditUser = () => {
- 更新用户信息 + {t('user.edit.title')}
{ { { <> { { )} - +
From ee3ed653568a8120eea417cfe36e4e19d34063d3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 23:48:05 +0800 Subject: [PATCH 07/57] feat: i18n support --- middleware/rate-limit.go | 3 +- .../public/locales/en/translation.json | 45 +++++++++++ .../public/locales/zh/translation.json | 45 +++++++++++ web/default/src/components/UsersTable.js | 78 +++++++++++-------- web/default/src/pages/User/index.js | 25 +++--- 5 files changed, 151 insertions(+), 45 deletions(-) diff --git a/middleware/rate-limit.go b/middleware/rate-limit.go index c1be92f3..63d7d549 100644 --- a/middleware/rate-limit.go +++ b/middleware/rate-limit.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" ) @@ -71,7 +72,7 @@ func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark s } func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) { - if maxRequestNum == 0 { + if maxRequestNum == 0 || config.DebugEnabled { return func(c *gin.Context) { c.Next() } diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index 0265b7b9..b65cff84 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -305,6 +305,7 @@ } }, "user": { + "title": "User Management", "edit": { "title": "Update User Information", "username": "Username", @@ -337,6 +338,50 @@ }, "messages": { "create_success": "User account created successfully!" + }, + "search": "Search users...", + "table": { + "id": "ID", + "username": "Username", + "group": "Group", + "quota": "Quota", + "role_text": "Role", + "status_text": "Status", + "actions": "Actions", + "remaining_quota": "Remaining Quota", + "used_quota": "Used Quota", + "request_count": "Request Count", + "role_types": { + "normal": "Normal User", + "admin": "Admin", + "super_admin": "Super Admin", + "unknown": "Unknown Role" + }, + "status_types": { + "activated": "Activated", + "banned": "Banned", + "unknown": "Unknown Status" + }, + "sort": { + "default": "Default Order", + "by_quota": "Sort by Remaining Quota", + "by_used_quota": "Sort by Used Quota", + "by_request_count": "Sort by Request Count" + }, + "sort_by": "Sort By" + }, + "buttons": { + "add": "Add New User", + "delete": "Delete", + "delete_user": "Delete User", + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "promote": "Promote", + "demote": "Demote" + }, + "messages": { + "operation_success": "Operation completed successfully!" } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index ff1bdbb0..c21f9507 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -305,6 +305,7 @@ } }, "user": { + "title": "用户管理", "edit": { "title": "更新用户信息", "username": "用户名", @@ -337,6 +338,50 @@ }, "messages": { "create_success": "用户账户创建成功!" + }, + "search": "搜索用户...", + "table": { + "id": "ID", + "username": "用户名", + "group": "分组", + "quota": "额度", + "role_text": "角色", + "status_text": "状态", + "actions": "操作", + "remaining_quota": "剩余额度", + "used_quota": "已用额度", + "request_count": "请求次数", + "role_types": { + "normal": "普通用户", + "admin": "管理员", + "super_admin": "超级管理员", + "unknown": "未知身份" + }, + "status_types": { + "activated": "已激活", + "banned": "已封禁", + "unknown": "未知状态" + }, + "sort": { + "default": "默认排序", + "by_quota": "按剩余额度排序", + "by_used_quota": "按已用额度排序", + "by_request_count": "按请求次数排序" + }, + "sort_by": "排序方式" + }, + "buttons": { + "add": "添加新的用户", + "delete": "删除", + "delete_user": "删除用户", + "enable": "启用", + "disable": "禁用", + "edit": "编辑", + "promote": "提升", + "demote": "降级" + }, + "messages": { + "operation_success": "操作成功完成!" } } } diff --git a/web/default/src/components/UsersTable.js b/web/default/src/components/UsersTable.js index 4951d104..d38fb218 100644 --- a/web/default/src/components/UsersTable.js +++ b/web/default/src/components/UsersTable.js @@ -20,16 +20,18 @@ import { renderText, } from '../helpers/render'; -function renderRole(role) { +function renderRole(role, t) { switch (role) { case 1: - return ; + return ; case 10: - return ; + return ; case 100: - return ; + return ( + + ); default: - return ; + return ; } } @@ -85,7 +87,7 @@ const UsersTable = () => { }); const { success, message } = res.data; if (success) { - showSuccess('操作成功完成!'); + showSuccess(t('user.messages.operation_success')); let user = res.data.data; let newUsers = [...users]; let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; @@ -105,17 +107,17 @@ const UsersTable = () => { const renderStatus = (status) => { switch (status) { case 1: - return ; + return ; case 2: return ( ); default: return ( ); } @@ -177,7 +179,7 @@ const UsersTable = () => { icon='search' fluid iconPosition='left' - placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...' + placeholder={t('user.search')} value={searchKeyword} loading={searching} onChange={handleKeywordChange} @@ -193,7 +195,7 @@ const UsersTable = () => { sortUser('id'); }} > - ID + {t('user.table.id')} { sortUser('username'); }} > - 用户名 + {t('user.table.username')} { sortUser('group'); }} > - 分组 + {t('user.table.group')} { sortUser('quota'); }} > - 统计信息 + {t('user.table.quota')} { sortUser('role'); }} > - 用户角色 + {t('user.table.role_text')} { sortUser('status'); }} > - 状态 + {t('user.table.status_text')} - 操作 + {t('user.table.actions')} @@ -267,23 +269,25 @@ const UsersTable = () => { {/**/} {renderQuota(user.quota, t)}} + content={t('user.table.remaining_quota')} + trigger={ + + } /> {renderQuota(user.used_quota, t)} } /> {renderNumber(user.request_count)} } /> - {renderRole(user.role)} + {renderRole(user.role, t)} {renderStatus(user.status)}
@@ -295,7 +299,7 @@ const UsersTable = () => { }} disabled={user.role === 100} > - 提升 + {t('user.buttons.promote')} { negative disabled={user.role === 100} > - 删除 + {t('user.buttons.delete')} } on='click' @@ -327,7 +331,7 @@ const UsersTable = () => { manageUser(user.username, 'delete', idx); }} > - 删除用户 {user.username} + {t('user.buttons.delete_user')} {user.username}
@@ -361,22 +367,26 @@ const UsersTable = () => { ( -
- - - 用户管理 - - - -
-); +const User = () => { + const { t } = useTranslation(); + + return ( +
+ + + {t('user.title')} + + + +
+ ); +}; export default User; From 33102c4586983003b846ffc1568953858fe1ec57 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 23:50:32 +0800 Subject: [PATCH 08/57] feat: i18n support --- .../public/locales/en/translation.json | 10 +-- .../public/locales/zh/translation.json | 10 +-- web/default/src/pages/User/AddUser.js | 90 ++++++++++--------- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index b65cff84..b11e66c7 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -330,14 +330,13 @@ "cancel": "Cancel" } }, - "messages": { - "update_success": "User information updated successfully!" - }, "add": { "title": "Create New User Account" }, "messages": { - "create_success": "User account created successfully!" + "update_success": "User information updated successfully!", + "create_success": "User account created successfully!", + "operation_success": "Operation completed successfully!" }, "search": "Search users...", "table": { @@ -379,9 +378,6 @@ "edit": "Edit", "promote": "Promote", "demote": "Demote" - }, - "messages": { - "operation_success": "Operation completed successfully!" } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index c21f9507..9fc0736c 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -330,14 +330,13 @@ "cancel": "取消" } }, - "messages": { - "update_success": "用户信息更新成功!" - }, "add": { "title": "创建新用户账户" }, "messages": { - "create_success": "用户账户创建成功!" + "update_success": "用户信息更新成功!", + "create_success": "用户账户创建成功!", + "operation_success": "操作成功完成!" }, "search": "搜索用户...", "table": { @@ -379,9 +378,6 @@ "edit": "编辑", "promote": "提升", "demote": "降级" - }, - "messages": { - "operation_success": "操作成功完成!" } } } diff --git a/web/default/src/pages/User/AddUser.js b/web/default/src/pages/User/AddUser.js index c261ab4c..c072b794 100644 --- a/web/default/src/pages/User/AddUser.js +++ b/web/default/src/pages/User/AddUser.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Form, Header, Segment } from 'semantic-ui-react'; +import { Button, Form, Card } from 'semantic-ui-react'; import { API, showError, showSuccess } from '../../helpers'; const AddUser = () => { @@ -30,49 +30,51 @@ const AddUser = () => { }; return ( - <> - -
{t('user.add.title')}
-
- - - - - - - - - - -
-
- +
+ + + {t('user.add.title')} +
+ + + + + + + + + + +
+
+
+
); }; From b7f008cd72df576dd21d9b01da81dd93a1551d16 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 23:52:42 +0800 Subject: [PATCH 09/57] feat: i18n support --- .../public/locales/en/translation.json | 23 ++++++++++++++++ .../public/locales/zh/translation.json | 23 ++++++++++++++++ web/default/src/pages/Dashboard/index.js | 26 ++++++++++++++++--- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index b11e66c7..c767a381 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -379,5 +379,28 @@ "promote": "Promote", "demote": "Demote" } + }, + "dashboard": { + "charts": { + "requests": { + "title": "Model Request Trend", + "tooltip": "Request Count" + }, + "quota": { + "title": "Quota Usage Trend", + "tooltip": "Quota Used" + }, + "tokens": { + "title": "Token Usage Trend", + "tooltip": "Token Count" + } + }, + "statistics": { + "title": "Statistics", + "tooltip": { + "date": "Date", + "value": "Value" + } + } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 9fc0736c..6bad3ae7 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -379,5 +379,28 @@ "promote": "提升", "demote": "降级" } + }, + "dashboard": { + "charts": { + "requests": { + "title": "模型请求趋势", + "tooltip": "请求次数" + }, + "quota": { + "title": "额度消费趋势", + "tooltip": "消费额度" + }, + "tokens": { + "title": "Token 消费趋势", + "tooltip": "Token 数量" + } + }, + "statistics": { + "title": "统计", + "tooltip": { + "date": "日期", + "value": "数值" + } + } } } diff --git a/web/default/src/pages/Dashboard/index.js b/web/default/src/pages/Dashboard/index.js index 03fd6451..fbb129f1 100644 --- a/web/default/src/pages/Dashboard/index.js +++ b/web/default/src/pages/Dashboard/index.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Card, Grid, Statistic } from 'semantic-ui-react'; import { LineChart, @@ -53,6 +54,7 @@ const chartConfig = { }; const Dashboard = () => { + const { t } = useTranslation(); const [data, setData] = useState([]); const [summaryData, setSummaryData] = useState({ todayRequests: 0, @@ -194,7 +196,7 @@ const Dashboard = () => { - 模型请求趋势 + {t('dashboard.charts.requests.title')} {summaryData.todayRequests}
@@ -220,6 +222,11 @@ const Dashboard = () => { borderRadius: '4px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', }} + formatter={(value) => [ + value, + t('dashboard.charts.requests.tooltip') + ]} + labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} /> { - 额度消费趋势 + {t('dashboard.charts.quota.title')} ${summaryData.todayQuota.toFixed(3)} @@ -268,6 +275,11 @@ const Dashboard = () => { borderRadius: '4px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', }} + formatter={(value) => [ + value, + t('dashboard.charts.quota.tooltip') + ]} + labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} /> { - Token 消费趋势 + {t('dashboard.charts.tokens.title')} {summaryData.todayTokens}
@@ -314,6 +326,11 @@ const Dashboard = () => { borderRadius: '4px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', }} + formatter={(value) => [ + value, + t('dashboard.charts.tokens.tooltip') + ]} + labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} /> { {/* 模型使用统计 */} - 统计 + {t('dashboard.statistics.title')}
@@ -361,6 +378,7 @@ const Dashboard = () => { borderRadius: '4px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', }} + labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} /> Date: Sat, 1 Feb 2025 23:58:55 +0800 Subject: [PATCH 10/57] feat: i18n support --- .../public/locales/en/translation.json | 37 +- .../public/locales/zh/translation.json | 37 +- web/default/src/components/LogsTable.js | 568 +++++++++--------- web/default/src/pages/Dashboard/index.js | 24 +- 4 files changed, 364 insertions(+), 302 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index c767a381..f4d74933 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -292,16 +292,43 @@ }, "log": { "title": "Operation Log", + "search": "Search logs...", "usage_details": "Usage Details", "total_quota": "Total Quota Used", "click_to_view": "Click to View", + "type": { + "select": "Select Log Type", + "all": "All", + "topup": "Top Up", + "usage": "Usage", + "admin": "Admin", + "system": "System", + "test": "Test" + }, "table": { - "id": "ID", - "username": "Username", + "time": "Time", + "channel": "Channel", "type": "Type", - "content": "Content", - "amount": "Amount", - "time": "Time" + "model": "Model", + "username": "Username", + "token_name": "Token Name", + "token_name_placeholder": "Optional", + "model_name": "Model Name", + "model_name_placeholder": "Optional", + "start_time": "Start Time", + "end_time": "End Time", + "channel_id": "Channel ID", + "channel_id_placeholder": "Optional", + "username_placeholder": "Optional", + "prompt_tokens": "Prompt Tokens", + "completion_tokens": "Completion Tokens", + "quota": "Quota", + "detail": "Detail" + }, + "buttons": { + "query": "Action", + "submit": "Query", + "refresh": "Refresh" } }, "user": { diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 6bad3ae7..d3b80c26 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -292,16 +292,43 @@ }, "log": { "title": "操作日志", + "search": "搜索日志...", "usage_details": "使用明细", "total_quota": "总消耗额度", "click_to_view": "点击查看", + "type": { + "select": "选择明细分类", + "all": "全部", + "topup": "充值", + "usage": "消费", + "admin": "管理", + "system": "系统", + "test": "测试" + }, "table": { - "id": "ID", - "username": "用户名", + "time": "时间", + "channel": "渠道", "type": "类型", - "content": "内容", - "amount": "数量", - "time": "时间" + "model": "模型", + "username": "用户名", + "token_name": "令牌名称", + "token_name_placeholder": "可选值", + "model_name": "模型名称", + "model_name_placeholder": "可选值", + "start_time": "起始时间", + "end_time": "结束时间", + "channel_id": "渠道 ID", + "channel_id_placeholder": "可选值", + "username_placeholder": "可选值", + "prompt_tokens": "提示词消耗", + "completion_tokens": "补全消耗", + "quota": "额度", + "detail": "详情" + }, + "buttons": { + "query": "操作", + "submit": "查询", + "refresh": "刷新" } }, "user": { diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index 5cf7ceba..b792cf30 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -8,6 +8,7 @@ import { Segment, Select, Table, + Popup, } from 'semantic-ui-react'; import { API, @@ -46,15 +47,6 @@ const MODE_OPTIONS = [ { key: 'self', text: '当前用户', value: 'self' }, ]; -const LOG_OPTIONS = [ - { key: '0', text: '全部', value: 0 }, - { key: '1', text: '充值', value: 1 }, - { key: '2', text: '消费', value: 2 }, - { key: '3', text: '管理', value: 3 }, - { key: '4', text: '系统', value: 4 }, - { key: '5', text: '测试', value: 5 }, -]; - function renderType(type) { switch (type) { case 1: @@ -170,6 +162,15 @@ const LogsTable = () => { token: 0, }); + const LOG_OPTIONS = [ + { key: '0', text: t('log.type.all'), value: 0 }, + { key: '1', text: t('log.type.topup'), value: 1 }, + { key: '2', text: t('log.type.usage'), value: 2 }, + { key: '3', text: t('log.type.admin'), value: 3 }, + { key: '4', text: t('log.type.system'), value: 4 }, + { key: '5', text: t('log.type.test'), value: 5 }, + ]; + const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -309,296 +310,295 @@ const LogsTable = () => { return ( <> - <> -
- {t('log.usage_details')}({t('log.total_quota')}: - {showStat && renderQuota(stat.quota, t)} - {!showStat && ( - - {t('log.click_to_view')} - - )} - ) -
-
- - - - - - - 查询 - - - {isAdminUser && ( - <> - - - - - - )} -
- - - - { - sortLog('created_time'); - }} +
+ {t('log.usage_details')}({t('log.total_quota')}: + {showStat && renderQuota(stat.quota, t)} + {!showStat && ( + + {t('log.click_to_view')} + + )} + ) +
+
+ + + + + + + {t('log.buttons.submit')} + + + {isAdminUser && ( + <> + + - 时间 -
- {isAdminUser && ( - { - sortLog('channel'); - }} - width={1} - > - 渠道 - - )} + value={channel} + placeholder={t('log.table.channel_id_placeholder')} + name='channel' + onChange={handleInputChange} + /> + + + + )} + setSearchKeyword(value)} + /> + +
+ + + { + sortLog('created_time'); + }} + width={3} + > + {t('log.table.time')} + + {isAdminUser && ( { - sortLog('type'); + sortLog('channel'); }} width={1} > - 类型 + {t('log.table.channel')} - { - sortLog('model_name'); - }} - width={2} - > - 模型 - - {showUserTokenQuota() && ( - <> - {isAdminUser && ( - { - sortLog('username'); - }} - width={1} - > - 用户 - - )} + )} + { + sortLog('type'); + }} + width={1} + > + {t('log.table.type')} + + { + sortLog('model_name'); + }} + width={2} + > + {t('log.table.model')} + + {showUserTokenQuota() && ( + <> + {isAdminUser && ( { - sortLog('token_name'); + sortLog('username'); }} - width={1} + width={2} > - 令牌 + {t('log.table.username')} - { - sortLog('prompt_tokens'); - }} - width={1} - > - 提示 - - { - sortLog('completion_tokens'); - }} - width={1} - > - 补全 - - { - sortLog('quota'); - }} - width={1} - > - 额度 - - - )} - { - sortLog('content'); - }} - width={isAdminUser ? 4 : 6} - > - 详情 - - - - - - {logs - .slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE - ) - .map((log, idx) => { - if (log.deleted) return <>; - return ( - - - {renderTimestamp(log.created_at, log.request_id)} - - {isAdminUser && ( - - {log.channel ? ( - - ) : ( - '' - )} - - )} - {renderType(log.type)} - - {log.model_name ? renderColorLabel(log.model_name) : ''} - - {showUserTokenQuota() && ( - <> - {isAdminUser && ( - - {log.username ? ( - - ) : ( - '' - )} - - )} - - {log.token_name - ? renderColorLabel(log.token_name) - : ''} - - - - {log.prompt_tokens ? log.prompt_tokens : ''} - - - {log.completion_tokens ? log.completion_tokens : ''} - - - {log.quota ? renderQuota(log.quota, t, 6) : ''} - - - )} - - {renderDetail(log)} - - ); - })} - - - - - -
- + width={2} + > + {t('log.table.token_name')} + + { + sortLog('prompt_tokens'); + }} + width={1} + > + {t('log.table.prompt_tokens')} + + { + sortLog('completion_tokens'); + }} + width={1} + > + {t('log.table.completion_tokens')} + + { + sortLog('quota'); + }} + width={1} + > + {t('log.table.quota')} + + + )} + {t('log.table.detail')} + + + + + {logs + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((log, idx) => { + if (log.deleted) return <>; + return ( + + + {renderTimestamp(log.created_at, log.request_id)} + + {isAdminUser && ( + + {log.channel ? ( + + ) : ( + '' + )} + + )} + {renderType(log.type)} + + {log.model_name ? renderColorLabel(log.model_name) : ''} + + {showUserTokenQuota() && ( + <> + {isAdminUser && ( + + {log.username ? ( + + ) : ( + '' + )} + + )} + + {log.token_name ? renderColorLabel(log.token_name) : ''} + + + + {log.prompt_tokens ? log.prompt_tokens : ''} + + + {log.completion_tokens ? log.completion_tokens : ''} + + + {log.quota ? renderQuota(log.quota, t, 6) : ''} + + + )} + + {renderDetail(log)} + + ); + })} + + + + + +