feat: i18n support

This commit is contained in:
JustSong 2025-02-01 23:58:55 +08:00
parent b7f008cd72
commit e183e3b9b0
4 changed files with 364 additions and 302 deletions

View File

@ -292,16 +292,43 @@
}, },
"log": { "log": {
"title": "Operation Log", "title": "Operation Log",
"search": "Search logs...",
"usage_details": "Usage Details", "usage_details": "Usage Details",
"total_quota": "Total Quota Used", "total_quota": "Total Quota Used",
"click_to_view": "Click to View", "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": { "table": {
"id": "ID", "time": "Time",
"username": "Username", "channel": "Channel",
"type": "Type", "type": "Type",
"content": "Content", "model": "Model",
"amount": "Amount", "username": "Username",
"time": "Time" "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": { "user": {

View File

@ -292,16 +292,43 @@
}, },
"log": { "log": {
"title": "操作日志", "title": "操作日志",
"search": "搜索日志...",
"usage_details": "使用明细", "usage_details": "使用明细",
"total_quota": "总消耗额度", "total_quota": "总消耗额度",
"click_to_view": "点击查看", "click_to_view": "点击查看",
"type": {
"select": "选择明细分类",
"all": "全部",
"topup": "充值",
"usage": "消费",
"admin": "管理",
"system": "系统",
"test": "测试"
},
"table": { "table": {
"id": "ID", "time": "时间",
"username": "用户名", "channel": "渠道",
"type": "类型", "type": "类型",
"content": "内容", "model": "模型",
"amount": "数量", "username": "用户名",
"time": "时间" "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": { "user": {

View File

@ -8,6 +8,7 @@ import {
Segment, Segment,
Select, Select,
Table, Table,
Popup,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { import {
API, API,
@ -46,15 +47,6 @@ const MODE_OPTIONS = [
{ key: 'self', text: '当前用户', value: 'self' }, { 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) { function renderType(type) {
switch (type) { switch (type) {
case 1: case 1:
@ -170,6 +162,15 @@ const LogsTable = () => {
token: 0, 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 }) => { const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
@ -309,296 +310,295 @@ const LogsTable = () => {
return ( return (
<> <>
<> <Header as='h3'>
<Header as='h3'> {t('log.usage_details')}{t('log.total_quota')}
{t('log.usage_details')}{t('log.total_quota')} {showStat && renderQuota(stat.quota, t)}
{showStat && renderQuota(stat.quota, t)} {!showStat && (
{!showStat && ( <span
<span onClick={handleEyeClick}
onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}
style={{ cursor: 'pointer', color: 'gray' }} >
> {t('log.click_to_view')}
{t('log.click_to_view')} </span>
</span> )}
)}
</Header>
</Header> <Form>
<Form> <Form.Group>
<Form.Group> <Form.Input
<Form.Input fluid
fluid label={t('log.table.token_name')}
label={'令牌名称'} width={3}
width={3} value={token_name}
value={token_name} placeholder={t('log.table.token_name_placeholder')}
placeholder={'可选值'} name='token_name'
name='token_name' onChange={handleInputChange}
onChange={handleInputChange} />
/> <Form.Input
<Form.Input fluid
fluid label={t('log.table.model_name')}
label='模型名称' width={3}
width={3} value={model_name}
value={model_name} placeholder={t('log.table.model_name_placeholder')}
placeholder='可选值' name='model_name'
name='model_name' onChange={handleInputChange}
onChange={handleInputChange} />
/> <Form.Input
<Form.Input fluid
fluid label={t('log.table.start_time')}
label='起始时间' width={4}
width={4} value={start_timestamp}
value={start_timestamp} type='datetime-local'
type='datetime-local' name='start_timestamp'
name='start_timestamp' onChange={handleInputChange}
onChange={handleInputChange} />
/> <Form.Input
<Form.Input fluid
fluid label={t('log.table.end_time')}
label='结束时间' width={4}
width={4} value={end_timestamp}
value={end_timestamp} type='datetime-local'
type='datetime-local' name='end_timestamp'
name='end_timestamp' onChange={handleInputChange}
onChange={handleInputChange} />
/> <Form.Button
<Form.Button fluid label='操作' width={2} onClick={refresh}> fluid
查询 label={t('log.buttons.query')}
</Form.Button> width={2}
</Form.Group> onClick={refresh}
{isAdminUser && ( >
<> {t('log.buttons.submit')}
<Form.Group> </Form.Button>
<Form.Input </Form.Group>
fluid {isAdminUser && (
label={'渠道 ID'} <>
width={3} <Form.Group>
value={channel} <Form.Input
placeholder='可选值' fluid
name='channel' label={t('log.table.channel_id')}
onChange={handleInputChange}
/>
<Form.Input
fluid
label={'用户名称'}
width={3}
value={username}
placeholder={'可选值'}
name='username'
onChange={handleInputChange}
/>
</Form.Group>
</>
)}
</Form>
<Table basic={'very'} compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('created_time');
}}
width={3} width={3}
> value={channel}
时间 placeholder={t('log.table.channel_id_placeholder')}
</Table.HeaderCell> name='channel'
{isAdminUser && ( onChange={handleInputChange}
<Table.HeaderCell />
style={{ cursor: 'pointer' }} <Form.Input
onClick={() => { fluid
sortLog('channel'); label={t('log.table.username')}
}} width={3}
width={1} value={username}
> placeholder={t('log.table.username_placeholder')}
渠道 name='username'
</Table.HeaderCell> onChange={handleInputChange}
)} />
</Form.Group>
</>
)}
<Form.Input
icon='search'
placeholder={t('log.search')}
value={searchKeyword}
onChange={(e, { value }) => setSearchKeyword(value)}
/>
</Form>
<Table basic={'very'} compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('created_time');
}}
width={3}
>
{t('log.table.time')}
</Table.HeaderCell>
{isAdminUser && (
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
sortLog('type'); sortLog('channel');
}} }}
width={1} width={1}
> >
类型 {t('log.table.channel')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell )}
style={{ cursor: 'pointer' }} <Table.HeaderCell
onClick={() => { style={{ cursor: 'pointer' }}
sortLog('model_name'); onClick={() => {
}} sortLog('type');
width={2} }}
> width={1}
模型 >
</Table.HeaderCell> {t('log.table.type')}
{showUserTokenQuota() && ( </Table.HeaderCell>
<> <Table.HeaderCell
{isAdminUser && ( style={{ cursor: 'pointer' }}
<Table.HeaderCell onClick={() => {
style={{ cursor: 'pointer' }} sortLog('model_name');
onClick={() => { }}
sortLog('username'); width={2}
}} >
width={1} {t('log.table.model')}
> </Table.HeaderCell>
用户 {showUserTokenQuota() && (
</Table.HeaderCell> <>
)} {isAdminUser && (
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
sortLog('token_name'); sortLog('username');
}} }}
width={1} width={2}
> >
令牌 {t('log.table.username')}
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell )}
style={{ cursor: 'pointer' }} <Table.HeaderCell
onClick={() => { style={{ cursor: 'pointer' }}
sortLog('prompt_tokens'); onClick={() => {
}} sortLog('token_name');
width={1}
>
提示
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('completion_tokens');
}}
width={1}
>
补全
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('quota');
}}
width={1}
>
额度
</Table.HeaderCell>
</>
)}
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('content');
}}
width={isAdminUser ? 4 : 6}
>
详情
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{logs
.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE
)
.map((log, idx) => {
if (log.deleted) return <></>;
return (
<Table.Row key={log.id}>
<Table.Cell>
{renderTimestamp(log.created_at, log.request_id)}
</Table.Cell>
{isAdminUser && (
<Table.Cell>
{log.channel ? (
<Label
basic
as={Link}
to={`/channel/edit/${log.channel}`}
>
{log.channel}
</Label>
) : (
''
)}
</Table.Cell>
)}
<Table.Cell>{renderType(log.type)}</Table.Cell>
<Table.Cell>
{log.model_name ? renderColorLabel(log.model_name) : ''}
</Table.Cell>
{showUserTokenQuota() && (
<>
{isAdminUser && (
<Table.Cell>
{log.username ? (
<Label
basic
as={Link}
to={`/user/edit/${log.user_id}`}
>
{log.username}
</Label>
) : (
''
)}
</Table.Cell>
)}
<Table.Cell>
{log.token_name
? renderColorLabel(log.token_name)
: ''}
</Table.Cell>
<Table.Cell>
{log.prompt_tokens ? log.prompt_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.completion_tokens ? log.completion_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.quota ? renderQuota(log.quota, t, 6) : ''}
</Table.Cell>
</>
)}
<Table.Cell>{renderDetail(log)}</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={'10'}>
<Select
placeholder='选择明细分类'
options={LOG_OPTIONS}
style={{ marginRight: '8px' }}
name='logType'
value={logType}
onChange={(e, { name, value }) => {
setLogType(value);
}} }}
/> width={2}
<Button size='small' onClick={refresh} loading={loading}> >
刷新 {t('log.table.token_name')}
</Button> </Table.HeaderCell>
<Pagination <Table.HeaderCell
floated='right' style={{ cursor: 'pointer' }}
activePage={activePage} onClick={() => {
onPageChange={onPaginationChange} sortLog('prompt_tokens');
size='small' }}
siblingRange={1} width={1}
totalPages={ >
Math.ceil(logs.length / ITEMS_PER_PAGE) + {t('log.table.prompt_tokens')}
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0) </Table.HeaderCell>
} <Table.HeaderCell
/> style={{ cursor: 'pointer' }}
</Table.HeaderCell> onClick={() => {
</Table.Row> sortLog('completion_tokens');
</Table.Footer> }}
</Table> width={1}
</> >
{t('log.table.completion_tokens')}
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('quota');
}}
width={1}
>
{t('log.table.quota')}
</Table.HeaderCell>
</>
)}
<Table.HeaderCell>{t('log.table.detail')}</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{logs
.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE
)
.map((log, idx) => {
if (log.deleted) return <></>;
return (
<Table.Row key={log.id}>
<Table.Cell>
{renderTimestamp(log.created_at, log.request_id)}
</Table.Cell>
{isAdminUser && (
<Table.Cell>
{log.channel ? (
<Label
basic
as={Link}
to={`/channel/edit/${log.channel}`}
>
{log.channel}
</Label>
) : (
''
)}
</Table.Cell>
)}
<Table.Cell>{renderType(log.type)}</Table.Cell>
<Table.Cell>
{log.model_name ? renderColorLabel(log.model_name) : ''}
</Table.Cell>
{showUserTokenQuota() && (
<>
{isAdminUser && (
<Table.Cell>
{log.username ? (
<Label
basic
as={Link}
to={`/user/edit/${log.user_id}`}
>
{log.username}
</Label>
) : (
''
)}
</Table.Cell>
)}
<Table.Cell>
{log.token_name ? renderColorLabel(log.token_name) : ''}
</Table.Cell>
<Table.Cell>
{log.prompt_tokens ? log.prompt_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.completion_tokens ? log.completion_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.quota ? renderQuota(log.quota, t, 6) : ''}
</Table.Cell>
</>
)}
<Table.Cell>{renderDetail(log)}</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={'10'}>
<Select
placeholder={t('log.type.select')}
options={LOG_OPTIONS}
style={{ marginRight: '8px' }}
name='logType'
value={logType}
onChange={(e, { name, value }) => {
setLogType(value);
}}
/>
<Button size='small' onClick={refresh} loading={loading}>
{t('log.buttons.refresh')}
</Button>
<Pagination
floated='right'
activePage={activePage}
onPageChange={onPaginationChange}
size='small'
siblingRange={1}
totalPages={
Math.ceil(logs.length / ITEMS_PER_PAGE) +
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</> </>
); );
}; };

View File

@ -86,7 +86,7 @@ const Dashboard = () => {
setSummaryData({ setSummaryData({
todayRequests: 0, todayRequests: 0,
todayQuota: 0, todayQuota: 0,
todayTokens: 0 todayTokens: 0,
}); });
return; return;
} }
@ -224,9 +224,11 @@ const Dashboard = () => {
}} }}
formatter={(value) => [ formatter={(value) => [
value, value,
t('dashboard.charts.requests.tooltip') t('dashboard.charts.requests.tooltip'),
]} ]}
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} labelFormatter={(label) =>
`${t('dashboard.tooltip.date')}: ${label}`
}
/> />
<Line <Line
type='monotone' type='monotone'
@ -277,9 +279,11 @@ const Dashboard = () => {
}} }}
formatter={(value) => [ formatter={(value) => [
value, value,
t('dashboard.charts.quota.tooltip') t('dashboard.charts.quota.tooltip'),
]} ]}
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} labelFormatter={(label) =>
`${t('dashboard.tooltip.date')}: ${label}`
}
/> />
<Line <Line
type='monotone' type='monotone'
@ -328,9 +332,11 @@ const Dashboard = () => {
}} }}
formatter={(value) => [ formatter={(value) => [
value, value,
t('dashboard.charts.tokens.tooltip') t('dashboard.charts.tokens.tooltip'),
]} ]}
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} labelFormatter={(label) =>
`${t('dashboard.tooltip.date')}: ${label}`
}
/> />
<Line <Line
type='monotone' type='monotone'
@ -378,7 +384,9 @@ const Dashboard = () => {
borderRadius: '4px', borderRadius: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
}} }}
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`} labelFormatter={(label) =>
`${t('dashboard.tooltip.date')}: ${label}`
}
/> />
<Legend <Legend
wrapperStyle={{ wrapperStyle={{