feat: Integrate i18n support and enhance UI text localization

- Added internationalization (i18n) support across various components, enabling dynamic language switching and improved user experience.
- Updated multiple components to utilize translation functions for labels, buttons, and messages, ensuring consistent language display.
- Enhanced the user interface by refining text elements in the ChannelsTable, LogsTable, and various settings pages, improving clarity and accessibility.
- Adjusted CSS styles for better responsiveness and layout consistency across different screen sizes.
This commit is contained in:
CalciumIon
2024-12-13 19:03:14 +08:00
parent cd21aa1c56
commit 221d7b5c99
42 changed files with 3192 additions and 1828 deletions

View File

@@ -36,43 +36,111 @@ import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
import { loadChannelModels } from './utils.js';
import EditTagModal from '../pages/Channel/EditTagModal.js';
import TextNumberInput from './custom/TextNumberInput.js';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
let type2label = undefined;
function renderType(type) {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
}
type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
}
return (
<Tag size="large" color={type2label[type]?.color}>
{type2label[type]?.text}
</Tag>
);
}
function renderTagType(type) {
return (
<Tag
color='light-blue'
prefixIcon={<IconList />}
size='large'
shape='circle'
type='light'
>
标签聚合
</Tag>
);
}
const ChannelsTable = () => {
const { t } = useTranslation();
let type2label = undefined;
const renderType = (type) => {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
}
type2label[0] = { value: 0, text: t('未知类型'), color: 'grey' };
}
return (
<Tag size="large" color={type2label[type]?.color}>
{type2label[type]?.text}
</Tag>
);
};
const renderTagType = () => {
return (
<Tag
color='light-blue'
prefixIcon={<IconList />}
size='large'
shape='circle'
type='light'
>
{t('标签聚合')}
</Tag>
);
};
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag size="large" color="green">
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size="large" color="yellow">
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size="large" color="yellow">
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size="large" color="grey">
{t('未知状态')}
</Tag>
);
}
};
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + t(' 秒');
if (responseTime === 0) {
return (
<Tag size="large" color="grey">
{t('未测试')}
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size="large" color="green">
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size="large" color="lime">
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size="large" color="yellow">
{time}
</Tag>
);
} else {
return (
<Tag size="large" color="red">
{time}
</Tag>
);
}
};
const columns = [
// {
// title: '',
@@ -80,15 +148,15 @@ const ChannelsTable = () => {
// className: 'checkbox',
// },
{
title: 'ID',
title: t('ID'),
dataIndex: 'id'
},
{
title: '名称',
title: t('名称'),
dataIndex: 'name'
},
{
title: '分组',
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
return (
@@ -103,18 +171,18 @@ const ChannelsTable = () => {
}
},
{
title: '类型',
title: t('类型'),
dataIndex: 'type',
render: (text, record, index) => {
if (record.children === undefined) {
return <>{renderType(text)}</>;
} else {
return <>{renderTagType(0)}</>;
return <>{renderTagType()}</>;
}
}
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
render: (text, record, index) => {
if (text === 3) {
@@ -126,7 +194,7 @@ const ChannelsTable = () => {
let time = otherInfo['status_time'];
return (
<div>
<Tooltip content={'原因:' + reason + ',时间:' + timestamp2string(time)}>
<Tooltip content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}>
{renderStatus(text)}
</Tooltip>
</div>
@@ -137,26 +205,26 @@ const ChannelsTable = () => {
}
},
{
title: '响应时间',
title: t('响应时间'),
dataIndex: 'response_time',
render: (text, record, index) => {
return <div>{renderResponseTime(text)}</div>;
}
},
{
title: '已用/剩余',
title: t('已用/剩余'),
dataIndex: 'expired_time',
render: (text, record, index) => {
if (record.children === undefined) {
return (
<div>
<Space spacing={1}>
<Tooltip content={'已用额度'}>
<Tooltip content={t('已用额度')}>
<Tag color="white" type="ghost" size="large">
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
<Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
<Tooltip content={t('剩余额度') + record.balance + t(',点击更新')}>
<Tag
color="white"
type="ghost"
@@ -172,7 +240,7 @@ const ChannelsTable = () => {
</div>
);
} else {
return <Tooltip content={'已用额度'}>
return <Tooltip content={t('已用额度')}>
<Tag color="white" type="ghost" size="large">
{renderQuota(record.used_quota)}
</Tag>
@@ -287,7 +355,7 @@ const ChannelsTable = () => {
<div>
<SplitButtonGroup
style={{ marginRight: 1 }}
aria-label="测试单个渠道操作项目组"
aria-label={t('测试单个渠道操作项目组')}
>
<Button
theme="light"
@@ -295,7 +363,7 @@ const ChannelsTable = () => {
testChannel(record, '');
}}
>
测试
{t('测试')}
</Button>
<Dropdown
trigger="click"
@@ -309,10 +377,9 @@ const ChannelsTable = () => {
></Button>
</Dropdown>
</SplitButtonGroup>
{/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
<Popconfirm
title="确定是否要删除此渠道?"
content="此修改将不可逆"
title={t('确定是否要删除此渠道?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -322,7 +389,7 @@ const ChannelsTable = () => {
}}
>
<Button theme="light" type="danger" style={{ marginRight: 1 }}>
删除
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -334,7 +401,7 @@ const ChannelsTable = () => {
manageChannel(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -345,7 +412,7 @@ const ChannelsTable = () => {
manageChannel(record.id, 'enable', record);
}}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -357,11 +424,11 @@ const ChannelsTable = () => {
setShowEdit(true);
}}
>
编辑
{t('编辑')}
</Button>
<Popconfirm
title="确定是否要复制此渠道?"
content="复制渠道的所有信息"
title={t('确定是否要复制此渠道?')}
content={t('复制渠道的所有信息')}
okType={'danger'}
position={'left'}
onConfirm={async () => {
@@ -369,7 +436,7 @@ const ChannelsTable = () => {
}}
>
<Button theme="light" type="primary" style={{ marginRight: 1 }}>
复制
{t('复制')}
</Button>
</Popconfirm>
</div>
@@ -385,7 +452,7 @@ const ChannelsTable = () => {
manageTag(record.key, 'enable');
}}
>
启用全部
{t('启用全部')}
</Button>
<Button
theme="light"
@@ -395,7 +462,7 @@ const ChannelsTable = () => {
manageTag(record.key, 'disable');
}}
>
禁用全部
{t('禁用全部')}
</Button>
<Button
theme="light"
@@ -406,7 +473,7 @@ const ChannelsTable = () => {
setEditingTag(record.key);
}}
>
编辑
{t('编辑')}
</Button>
</>
);
@@ -703,71 +770,6 @@ const ChannelsTable = () => {
}
};
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag size="large" color="green">
已启用
</Tag>
);
case 2:
return (
<Tag size="large" color="yellow">
已禁用
</Tag>
);
case 3:
return (
<Tag size="large" color="yellow">
自动禁用
</Tag>
);
default:
return (
<Tag size="large" color="grey">
未知状态
</Tag>
);
}
};
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + ' 秒';
if (responseTime === 0) {
return (
<Tag size="large" color="grey">
未测试
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size="large" color="green">
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size="large" color="lime">
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size="large" color="yellow">
{time}
</Tag>
);
} else {
return (
<Tag size="large" color="red">
{time}
</Tag>
);
}
};
const searchChannels = async (searchKeyword, searchGroup, searchModel, enableTagMode) => {
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
await loadChannels(0, pageSize, idSort, enableTagMode);
@@ -794,7 +796,7 @@ const ChannelsTable = () => {
if (success) {
record.response_time = time * 1000;
record.test_time = Date.now() / 1000;
showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2)));
} else {
showError(message);
}
@@ -804,7 +806,7 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/test`);
const { success, message } = res.data;
if (success) {
showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。'));
} else {
showError(message);
}
@@ -814,7 +816,7 @@ const ChannelsTable = () => {
const res = await API.delete(`/api/channel/disabled`);
const { success, message, data } = res.data;
if (success) {
showSuccess(`已删除所有禁用渠道,共计 ${data}`);
showSuccess(t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data));
await refresh();
} else {
showError(message);
@@ -827,7 +829,7 @@ const ChannelsTable = () => {
if (success) {
record.balance = balance;
record.balance_updated_time = Date.now() / 1000;
showInfo(`通道 ${record.name} 余额更新成功!`);
showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name));
} else {
showError(message);
}
@@ -838,7 +840,7 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/update_balance`);
const { success, message } = res.data;
if (success) {
showInfo('已更新完毕所有已启用通道余额!');
showInfo(t('已更新完毕所有已启用通道余额!'));
} else {
showError(message);
}
@@ -847,7 +849,7 @@ const ChannelsTable = () => {
const batchDeleteChannels = async () => {
if (selectedChannels.length === 0) {
showError('请先选择要删除的通道!');
showError(t('请先选择要删除的通道!'));
return;
}
setLoading(true);
@@ -858,7 +860,7 @@ const ChannelsTable = () => {
const res = await API.post(`/api/channel/batch`, { ids: ids });
const { success, message, data } = res.data;
if (success) {
showSuccess(`已删除 ${data} 个通道!`);
showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
await refresh();
} else {
showError(message);
@@ -870,7 +872,7 @@ const ChannelsTable = () => {
const res = await API.post(`/api/channel/fix`);
const { success, message, data } = res.data;
if (success) {
showSuccess(`已修复 ${data} 个通道!`);
showSuccess(t('已修复 ${data} 个通道!').replace('${data}', data));
await refresh();
} else {
showError(message);
@@ -990,8 +992,8 @@ const ChannelsTable = () => {
<Space>
<Form.Input
field="search_keyword"
label="搜索渠道关键词"
placeholder="ID名称和密钥 ..."
label={t('搜索渠道关键词')}
placeholder={t('搜索渠道的 ID名称和密钥 ...')}
value={searchKeyword}
loading={searching}
onChange={(v) => {
@@ -1000,8 +1002,8 @@ const ChannelsTable = () => {
/>
<Form.Input
field="search_model"
label="模型"
placeholder="模型关键字"
label={t('模型')}
placeholder={t('模型关键字')}
value={searchModel}
loading={searching}
onChange={(v) => {
@@ -1010,8 +1012,8 @@ const ChannelsTable = () => {
/>
<Form.Select
field="group"
label="分组"
optionList={[{ label: '选择分组', value: null }, ...groupOptions]}
label={t('分组')}
optionList={[{ label: t('选择分组'), value: null }, ...groupOptions]}
initValue={null}
onChange={(v) => {
setSearchGroup(v);
@@ -1019,13 +1021,13 @@ const ChannelsTable = () => {
}}
/>
<Button
label="查询"
label={t('查询')}
type="primary"
htmlType="submit"
className="btn-margin-right"
style={{ marginRight: 8 }}
>
查询
{t('查询')}
</Button>
</Space>
</div>
@@ -1042,12 +1044,12 @@ const ChannelsTable = () => {
<Space
style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
>
<Typography.Text strong>使用ID排序</Typography.Text>
<Typography.Text strong>{t('使用ID排序')}</Typography.Text>
<Switch
checked={idSort}
label="使用ID排序"
uncheckedText="关"
aria-label="是否用ID排序"
label={t('使用ID排序')}
uncheckedText={t('关')}
aria-label={t('是否用ID排序')}
onChange={(v) => {
localStorage.setItem('id-sort', v + '');
setIdSort(v);
@@ -1069,35 +1071,35 @@ const ChannelsTable = () => {
setShowEdit(true);
}}
>
添加渠道
{t('添加渠道')}
</Button>
<Popconfirm
title="确定?"
title={t('确定?')}
okType={'warning'}
onConfirm={testAllChannels}
position={isMobile() ? 'top' : 'top'}
>
<Button theme="light" type="warning" style={{ marginRight: 8 }}>
测试所有通道
{t('测试所有通道')}
</Button>
</Popconfirm>
<Popconfirm
title="确定?"
title={t('确定?')}
okType={'secondary'}
onConfirm={updateAllChannelsBalance}
>
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
更新所有已启用通道余额
{t('更新所有已启用通道余额')}
</Button>
</Popconfirm>
<Popconfirm
title="确定是否要删除禁用通道?"
content="此修改将不可逆"
title={t('确定是否要删除禁用通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={deleteAllDisabledChannels}
>
<Button theme="light" type="danger" style={{ marginRight: 8 }}>
删除禁用通道
{t('删除禁用通道')}
</Button>
</Popconfirm>
@@ -1107,24 +1109,24 @@ const ChannelsTable = () => {
style={{ marginRight: 8 }}
onClick={refresh}
>
刷新
{t('刷新')}
</Button>
</Space>
</div>
<div style={{ marginTop: 20 }}>
<Space>
<Typography.Text strong>开启批量删除</Typography.Text>
<Typography.Text strong>{t('开启批量删除')}</Typography.Text>
<Switch
label="开启批量删除"
uncheckedText="关"
aria-label="是否开启批量删除"
label={t('开启批量删除')}
uncheckedText={t('关')}
aria-label={t('是否开启批量删除')}
onChange={(v) => {
setEnableBatchDelete(v);
}}
></Switch>
<Popconfirm
title="确定是否要删除所选通道?"
content="此修改将不可逆"
title={t('确定是否要删除所选通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={batchDeleteChannels}
disabled={!enableBatchDelete}
@@ -1136,33 +1138,32 @@ const ChannelsTable = () => {
type="danger"
style={{ marginRight: 8 }}
>
删除所选通道
{t('删除所选通道')}
</Button>
</Popconfirm>
<Popconfirm
title="确定是否要修复数据库一致性?"
content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
title={t('确定是否要修复数据库一致性?')}
content={t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用')}
okType={'warning'}
onConfirm={fixChannelsAbilities}
position={'top'}
>
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
修复数据库一致性
{t('修复数据库一致性')}
</Button>
</Popconfirm>
</Space>
</div>
<div style={{ marginTop: 20 }}>
<Space>
<Typography.Text strong>标签聚合模式</Typography.Text>
<Typography.Text strong>{t('标签聚合模式')}</Typography.Text>
<Switch
checked={enableTagMode}
label="标签聚合模式"
uncheckedText="关"
aria-label="是否启用标签聚合"
label={t('标签聚合模式')}
uncheckedText={t('关')}
aria-label={t('是否启用标签聚合')}
onChange={(v) => {
setEnableTagMode(v);
// 切换模式时重新加载数据
loadChannels(0, pageSize, idSort, v);
}}
/>

View File

@@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui';
const FooterBar = () => {
const { t } = useTranslation();
const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML());
let remainCheckTimes = 5;
@@ -24,7 +25,7 @@ const FooterBar = () => {
>
New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
</a>
{' '}
{t('由')}{' '}
<a
href='https://github.com/Calcium-Ion'
target='_blank'
@@ -32,7 +33,7 @@ const FooterBar = () => {
>
Calcium-Ion
</a>{' '}
开发基于{' '}
{t('开发,基于')}{' '}
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { useSetTheme, useTheme } from '../context/Theme';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
import '../index.css';
@@ -16,7 +17,8 @@ import {
IconKey, IconMenu,
IconNoteMoneyStroked,
IconPriceTag,
IconUser
IconUser,
IconLanguage
} from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render';
@@ -42,41 +44,45 @@ if (localStorage.getItem('chat_link')) {
}
const HeaderBar = () => {
const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
let navigate = useNavigate();
const [currentLang, setCurrentLang] = useState(i18n.language);
const systemName = getSystemName();
const logo = getLogo();
const currentDate = new Date();
// enable fireworks on new year(1.1 and 2.9-2.24)
const isNewYear =
(currentDate.getMonth() === 0 && currentDate.getDate() === 1) ||
(currentDate.getMonth() === 1 &&
currentDate.getDate() >= 9 &&
currentDate.getDate() <= 24);
(currentDate.getMonth() === 0 && currentDate.getDate() === 1);
let buttons = [
{
text: '首页',
text: t('首页'),
itemKey: 'home',
to: '/',
},
{
text: '控制台',
text: t('控制台'),
itemKey: 'detail',
to: '/',
},
{
text: '定价',
text: t('定价'),
itemKey: 'pricing',
to: '/pricing',
},
{
text: t('关于'),
itemKey: 'about',
to: '/about',
},
];
async function logout() {
await API.get('/api/user/logout');
showSuccess('注销成功!');
showSuccess(t('注销成功!'));
userDispatch({ type: 'logout' });
localStorage.removeItem('user');
navigate('/login');
@@ -106,11 +112,28 @@ const HeaderBar = () => {
}
}, []);
useEffect(() => {
const handleLanguageChanged = (lng) => {
setCurrentLang(lng);
};
i18n.on('languageChanged', handleLanguageChanged);
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, [i18n]);
const handleLanguageChange = (lang) => {
i18n.changeLanguage(lang);
};
return (
<>
<Layout>
<div style={{ width: '100%' }}>
<Nav
className={'topnav'}
mode={'horizontal'}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
const routerMap = {
@@ -125,10 +148,10 @@ const HeaderBar = () => {
<div onClick={(e) => {
if (props.itemKey === 'home') {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
styleDispatch({ type: 'SET_SIDER', payload: false });
// styleDispatch({ type: 'SET_SIDER', payload: false });
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
styleDispatch({ type: 'SET_SIDER', payload: true });
// styleDispatch({ type: 'SET_SIDER', payload: true });
}
}}>
<Link
@@ -149,10 +172,10 @@ const HeaderBar = () => {
<>
{
!styleState.showSider ?
<Button icon={<IconMenu />} theme="light" aria-label="展开侧边栏" onClick={
<Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: true })
} />:
<Button icon={<IconIndentLeft />} theme="light" aria-label="关闭侧边栏" onClick={
<Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: false })
} />
}
@@ -182,7 +205,7 @@ const HeaderBar = () => {
<Nav.Item itemKey={'new-year'} text={'🏮'} />
</Dropdown>
)}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
{/* <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> */}
<>
<Switch
checkedText='🌞'
@@ -194,13 +217,37 @@ const HeaderBar = () => {
}}
/>
</>
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Item
onClick={() => handleLanguageChange('zh')}
type={currentLang === 'zh' ? 'primary' : 'tertiary'}
>
中文
</Dropdown.Item>
<Dropdown.Item
onClick={() => handleLanguageChange('en')}
type={currentLang === 'en' ? 'primary' : 'tertiary'}
>
English
</Dropdown.Item>
</Dropdown.Menu>
}
>
<Nav.Item
itemKey={'language'}
icon={<IconLanguage />}
/>
</Dropdown>
{userState.user ? (
<>
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Item onClick={logout}>退出</Dropdown.Item>
<Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
</Dropdown.Menu>
}
>
@@ -218,14 +265,18 @@ const HeaderBar = () => {
<>
<Nav.Item
itemKey={'login'}
text={'登录'}
// icon={<IconKey />}
/>
<Nav.Item
itemKey={'register'}
text={'注册'}
text={!styleState.isMobile?t('登录'):null}
icon={<IconUser />}
/>
{
!styleState.isMobile && (
<Nav.Item
itemKey={'register'}
text={t('注册')}
icon={<IconKey />}
/>
)
}
</>
)}
</>

View File

@@ -28,6 +28,7 @@ import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
import WeChatIcon from './WeChatIcon';
import { setUserData } from '../helpers/data.js';
import LinuxDoIcon from './LinuxDoIcon.js';
import { useTranslation } from 'react-i18next';
const LoginForm = () => {
const [inputs, setInputs] = useState({
@@ -45,6 +46,7 @@ const LoginForm = () => {
let navigate = useNavigate();
const [status, setStatus] = useState({});
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const { t } = useTranslation();
const logo = getLogo();
@@ -55,7 +57,7 @@ const LoginForm = () => {
useEffect(() => {
if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录');
showError(t('未登录或登录已过期,请重新登录'));
}
let status = localStorage.getItem('status');
if (status) {
@@ -182,20 +184,20 @@ const LoginForm = () => {
<div style={{ width: 500 }}>
<Card>
<Title heading={2} style={{ textAlign: 'center' }}>
用户登录
{t('用户登录')}
</Title>
<Form>
<Form.Input
field={'username'}
label={'用户名'}
placeholder='用户名'
label={t('用户名/邮箱')}
placeholder={t('用户名/邮箱')}
name='username'
onChange={(value) => handleChange('username', value)}
/>
<Form.Input
field={'password'}
label={'密码'}
placeholder='密码'
label={t('密码')}
placeholder={t('密码')}
name='password'
type='password'
onChange={(value) => handleChange('password', value)}
@@ -209,7 +211,7 @@ const LoginForm = () => {
htmlType={'submit'}
onClick={handleSubmit}
>
登录
{t('登录')}
</Button>
</Form>
<div
@@ -220,10 +222,10 @@ const LoginForm = () => {
}}
>
<Text>
没有账号请先 <Link to='/register'>注册账号</Link>
{t('没有账户?')} <Link to='/register'>{t('点击注册')}</Link>
</Text>
<Text>
忘记密码 <Link to='/reset'>点击重置</Link>
{t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link>
</Text>
</div>
{status.github_oauth ||
@@ -232,7 +234,7 @@ const LoginForm = () => {
status.linuxdo_oauth ? (
<>
<Divider margin='12px' align='center'>
第三方登录
{t('第三方登录')}
</Divider>
<div
style={{
@@ -296,12 +298,12 @@ const LoginForm = () => {
<></>
)}
<Modal
title='微信扫码登录'
title={t('微信扫码登录')}
visible={showWeChatLoginModal}
maskClosable={true}
onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)}
okText={'登录'}
okText={t('登录')}
size={'small'}
centered={true}
>
@@ -316,14 +318,14 @@ const LoginForm = () => {
</div>
<div style={{ textAlign: 'center' }}>
<p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p>
</div>
<Form size='large'>
<Form.Input
field={'wechat_verification_code'}
placeholder='验证码'
label={'验证码'}
placeholder={t('验证码')}
label={t('验证码')}
value={inputs.wechat_verification_code}
onChange={(value) =>
handleChange('wechat_verification_code', value)

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
API,
copy,
@@ -40,8 +41,8 @@ function renderTimestamp(timestamp) {
}
const MODE_OPTIONS = [
{ key: 'all', text: '全部用户', value: 'all' },
{ key: 'self', text: '当前用户', value: 'self' },
{ key: 'all', text: 'all', value: 'all' },
{ key: 'self', text: 'current user', value: 'self' },
];
const colors = [
@@ -62,123 +63,92 @@ const colors = [
'yellow',
];
function renderType(type) {
switch (type) {
case 1:
const LogsTable = () => {
const { t } = useTranslation();
function renderType(type) {
switch (type) {
case 1:
return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
case 2:
return <Tag color='lime' size='large'>{t('消费')}</Tag>;
case 3:
return <Tag color='orange' size='large'>{t('管理')}</Tag>;
case 4:
return <Tag color='purple' size='large'>{t('系统')}</Tag>;
default:
return <Tag color='black' size='large'>{t('未知')}</Tag>;
}
}
function renderIsStream(bool) {
if (bool) {
return <Tag color='blue' size='large'>{t('流')}</Tag>;
} else {
return <Tag color='purple' size='large'>{t('非流')}</Tag>;
}
}
function renderUseTime(type) {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='cyan' size='large'>
<Tag color='green' size='large'>
{' '}
充值{' '}
{time} s{' '}
</Tag>
);
case 2:
return (
<Tag color='lime' size='large'>
{' '}
消费{' '}
</Tag>
);
case 3:
} else if (time < 300) {
return (
<Tag color='orange' size='large'>
{' '}
管理{' '}
{time} s{' '}
</Tag>
);
case 4:
} else {
return (
<Tag color='purple' size='large'>
<Tag color='red' size='large'>
{' '}
系统{' '}
{time} s{' '}
</Tag>
);
default:
}
}
function renderFirstUseTime(type) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='black' size='large'>
<Tag color='green' size='large'>
{' '}
未知{' '}
{time} s{' '}
</Tag>
);
}
}
} else if (time < 10) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderIsStream(bool) {
if (bool) {
return (
<Tag color='blue' size='large'>
</Tag>
);
} else {
return (
<Tag color='purple' size='large'>
非流
</Tag>
);
}
}
function renderUseTime(type) {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='green' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 300) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderFirstUseTime(type) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='green' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 10) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
const LogsTable = () => {
const columns = [
{
title: '时间',
title: t('时间'),
dataIndex: 'timestamp2string',
},
{
title: '渠道',
title: t('渠道'),
dataIndex: 'channel',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -204,7 +174,7 @@ const LogsTable = () => {
},
},
{
title: '用户',
title: t('用户'),
dataIndex: 'username',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -226,7 +196,7 @@ const LogsTable = () => {
},
},
{
title: '令牌',
title: t('令牌'),
dataIndex: 'token_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -239,7 +209,7 @@ const LogsTable = () => {
}}
>
{' '}
{text}{' '}
{t(text)}{' '}
</Tag>
</div>
) : (
@@ -248,14 +218,14 @@ const LogsTable = () => {
},
},
{
title: '类型',
title: t('类型'),
dataIndex: 'type',
render: (text, record, index) => {
return <>{renderType(text)}</>;
},
},
{
title: '模型',
title: t('模型'),
dataIndex: 'model_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -277,7 +247,7 @@ const LogsTable = () => {
},
},
{
title: '用时/首字',
title: t('用时/首字'),
dataIndex: 'use_time',
render: (text, record, index) => {
if (record.is_stream) {
@@ -304,7 +274,7 @@ const LogsTable = () => {
},
},
{
title: '提示',
title: t('提示'),
dataIndex: 'prompt_tokens',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -315,7 +285,7 @@ const LogsTable = () => {
},
},
{
title: '补全',
title: t('补全'),
dataIndex: 'completion_tokens',
render: (text, record, index) => {
return parseInt(text) > 0 &&
@@ -327,7 +297,7 @@ const LogsTable = () => {
},
},
{
title: '花费',
title: t('花费'),
dataIndex: 'quota',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -338,11 +308,11 @@ const LogsTable = () => {
},
},
{
title: '重试',
title: t('重试'),
dataIndex: 'retry',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
let content = '渠道' + record.channel;
let content = t('渠道') + `${record.channel}`;
if (record.other !== '') {
let other = JSON.parse(record.other);
if (other === null) {
@@ -357,7 +327,7 @@ const LogsTable = () => {
// channel id array
let useChannel = other.admin_info.use_channel;
let useChannelStr = useChannel.join('->');
content = `渠道${useChannelStr}`;
content = t('渠道') + `${useChannelStr}`;
}
}
}
@@ -365,7 +335,7 @@ const LogsTable = () => {
},
},
{
title: '详情',
title: t('详情'),
dataIndex: 'content',
render: (text, record, index) => {
let other = getLogOther(record.other);
@@ -493,13 +463,13 @@ const LogsTable = () => {
const { success, message, data } = res.data;
if (success) {
Modal.info({
title: '用户信息',
title: t('用户信息'),
content: (
<div style={{ padding: 12 }}>
<p>用户名: {data.username}</p>
<p>余额: {renderQuota(data.quota)}</p>
<p>已用额度{renderQuota(data.used_quota)}</p>
<p>请求次数{renderNumber(data.request_count)}</p>
<p>{t('用户名')}: {data.username}</p>
<p>{t('余额')}: {renderQuota(data.quota)}</p>
<p>{t('已用额度')}{renderQuota(data.used_quota)}</p>
<p>{t('请求次数')}{renderNumber(data.request_count)}</p>
</div>
),
centered: true,
@@ -537,26 +507,26 @@ const LogsTable = () => {
}
if (other?.ws || other?.audio) {
expandDataLocal.push({
key: '语音输入',
key: t('语音输入'),
value: other.audio_input,
});
expandDataLocal.push({
key: '语音输出',
key: t('语音输出'),
value: other.audio_output,
});
expandDataLocal.push({
key: '文字输入',
key: t('文字输入'),
value: other.text_input,
});
expandDataLocal.push({
key: '文字输出',
key: t('文字输出'),
value: other.text_output,
});
}
expandDataLocal.push({
key: '日志详情',
key: t('日志详情'),
value: logs[i].content,
})
});
if (logs[i].type === 2) {
let content = '';
if (other?.ws || other?.audio) {
@@ -583,7 +553,7 @@ const LogsTable = () => {
);
}
expandDataLocal.push({
key: '计费过程',
key: t('计费过程'),
value: content,
});
}
@@ -676,7 +646,7 @@ const LogsTable = () => {
<Spin spinning={loadingStat}>
<Space>
<Tag color='green' size='large' style={{ padding: 15 }}>
总消耗额度: {renderQuota(stat.quota)}
{t('总消耗额度')}: {renderQuota(stat.quota)}
</Tag>
<Tag color='blue' size='large' style={{ padding: 15 }}>
RPM: {stat.rpm}
@@ -691,25 +661,25 @@ const LogsTable = () => {
<>
<Form.Input
field='token_name'
label='令牌名称'
label={t('令牌名称')}
style={{ width: 176 }}
value={token_name}
placeholder={'可选值'}
placeholder={t('可选值')}
name='token_name'
onChange={(value) => handleInputChange(value, 'token_name')}
/>
<Form.Input
field='model_name'
label='模型名称'
label={t('模型名称')}
style={{ width: 176 }}
value={model_name}
placeholder='可选值'
placeholder={t('可选值')}
name='model_name'
onChange={(value) => handleInputChange(value, 'model_name')}
/>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
@@ -720,7 +690,7 @@ const LogsTable = () => {
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
@@ -732,26 +702,26 @@ const LogsTable = () => {
<>
<Form.Input
field='channel'
label='渠道 ID'
label={t('渠道 ID')}
style={{ width: 176 }}
value={channel}
placeholder='可选值'
placeholder={t('可选值')}
name='channel'
onChange={(value) => handleInputChange(value, 'channel')}
/>
<Form.Input
field='username'
label='用户名称'
label={t('用户名称')}
style={{ width: 176 }}
value={username}
placeholder={'可选值'}
placeholder={t('可选值')}
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
</>
)}
<Button
label='查询'
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
@@ -759,7 +729,7 @@ const LogsTable = () => {
loading={loading}
style={{ marginTop: 24 }}
>
查询
{t('查询')}
</Button>
<Form.Section></Form.Section>
</>
@@ -773,11 +743,11 @@ const LogsTable = () => {
loadLogs(0, pageSize, parseInt(value));
}}
>
<Select.Option value='0'>全部</Select.Option>
<Select.Option value='1'>充值</Select.Option>
<Select.Option value='2'>消费</Select.Option>
<Select.Option value='3'>管理</Select.Option>
<Select.Option value='4'>系统</Select.Option>
<Select.Option value='0'>{t('全部')}</Select.Option>
<Select.Option value='1'>{t('充值')}</Select.Option>
<Select.Option value='2'>{t('消费')}</Select.Option>
<Select.Option value='3'>{t('管理')}</Select.Option>
<Select.Option value='4'>{t('系统')}</Select.Option>
</Select>
</div>
<Table

View File

@@ -21,6 +21,7 @@ import {
Typography,
} from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants';
import { useTranslation } from 'react-i18next';
const colors = [
'amber',
@@ -40,247 +41,245 @@ const colors = [
'yellow',
];
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large'>
绘图
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large'>
放大
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large'>
变换
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large'>
强变换
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large'>
弱变换
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large'>
平移
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large'>
图生文
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large'>
图混合
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large'>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large'>
缩词
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large'>
重绘
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large'>
局部重绘-提交
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large'>
变焦
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large'>
自定义变焦-提交
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large'>
窗口处理
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large'>
换脸
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' size='large'>
已提交
</Tag>
);
case 21:
return (
<Tag color='lime' size='large'>
等待中
</Tag>
);
case 22:
return (
<Tag color='orange' size='large'>
重复提交
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large'>
未提交
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
function renderStatus(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large'>
成功
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large'>
未启动
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large'>
队列中
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large'>
执行中
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large'>
失败
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large'>
窗口等待
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
// 确保startTime和finishTime都是有效的时间戳
if (!submit_time || !finishTime) return 'N/A';
// 将时间戳转换为Date对象
const start = new Date(submit_time);
const finish = new Date(finishTime);
// 计算时间差(毫秒)
const durationMs = finish - start;
// 将时间差转换为秒,并保留一位小数
const durationSec = (durationMs / 1000).toFixed(1);
// 设置颜色大于60秒则为红色小于等于60秒则为绿色
const color = durationSec > 60 ? 'red' : 'green';
// 返回带有样式的颜色标签
return (
<Tag color={color} size="large">
{durationSec}
</Tag>
);
}
const LogsTable = () => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
function renderType(type) {
const { t } = useTranslation();
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large'>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large'>
{t('放大')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large'>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large'>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large'>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large'>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large'>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large'>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large'>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large'>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large'>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large'>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large'>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large'>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large'>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large'>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
function renderCode(code) {
const { t } = useTranslation();
switch (code) {
case 1:
return (
<Tag color='green' size='large'>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' size='large'>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' size='large'>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large'>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
function renderStatus(type) {
const { t } = useTranslation();
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large'>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large'>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large'>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large'>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large'>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large'>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
const { t } = useTranslation();
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} size="large">
{durationSec} {t('秒')}
</Tag>
);
}
const columns = [
{
title: '提交时间',
title: t('提交时间'),
dataIndex: 'submit_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>;
},
},
{
title: '花费时间',
title: t('花费时间'),
dataIndex: 'finish_time', // 以finish_time作为dataIndex
key: 'finish_time',
render: (finish, record) => {
@@ -289,7 +288,7 @@ const LogsTable = () => {
},
},
{
title: '渠道',
title: t('渠道'),
dataIndex: 'channel_id',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -310,21 +309,21 @@ const LogsTable = () => {
},
},
{
title: '类型',
title: t('类型'),
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text)}</div>;
},
},
{
title: '任务ID',
title: t('任务ID'),
dataIndex: 'mj_id',
render: (text, record, index) => {
return <div>{text}</div>;
},
},
{
title: '提交结果',
title: t('提交结果'),
dataIndex: 'code',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -332,7 +331,7 @@ const LogsTable = () => {
},
},
{
title: '任务状态',
title: t('任务状态'),
dataIndex: 'status',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -340,7 +339,7 @@ const LogsTable = () => {
},
},
{
title: '进度',
title: t('进度'),
dataIndex: 'progress',
render: (text, record, index) => {
return (
@@ -363,11 +362,11 @@ const LogsTable = () => {
},
},
{
title: '结果图片',
title: t('结果图片'),
dataIndex: 'image_url',
render: (text, record, index) => {
if (!text) {
return '无';
return t('无');
}
return (
<Button
@@ -376,7 +375,7 @@ const LogsTable = () => {
setIsModalOpenurl(true); // 打开模态框
}}
>
查看图片
{t('查看图片')}
</Button>
);
},
@@ -387,7 +386,7 @@ const LogsTable = () => {
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
return t('无');
}
return (
@@ -410,7 +409,7 @@ const LogsTable = () => {
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
return t('无');
}
return (
@@ -428,12 +427,12 @@ const LogsTable = () => {
},
},
{
title: '失败原因',
title: t('失败原因'),
dataIndex: 'fail_reason',
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
return t('无');
}
return (
@@ -565,7 +564,7 @@ const LogsTable = () => {
{isAdminUser && showBanner ? (
<Banner
type='info'
description='当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。'
description={t('当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。')}
/>
) : (
<></>
@@ -574,25 +573,25 @@ const LogsTable = () => {
<>
<Form.Input
field='channel_id'
label='渠道 ID'
label={t('渠道 ID')}
style={{ width: 176 }}
value={channel_id}
placeholder={'可选值'}
placeholder={t('可选值')}
name='channel_id'
onChange={(value) => handleInputChange(value, 'channel_id')}
/>
<Form.Input
field='mj_id'
label='任务 ID'
label={t('任务 ID')}
style={{ width: 176 }}
value={mj_id}
placeholder='可选值'
placeholder={t('可选值')}
name='mj_id'
onChange={(value) => handleInputChange(value, 'mj_id')}
/>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
@@ -603,7 +602,7 @@ const LogsTable = () => {
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
@@ -614,13 +613,13 @@ const LogsTable = () => {
<Form.Section>
<Button
label='查询'
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
>
查询
{t('查询')}
</Button>
</Form.Section>
</>
@@ -635,6 +634,12 @@ const LogsTable = () => {
total: logCount,
pageSizeOpts: [10, 20, 50, 100],
onPageChange: handlePageChange,
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: logCount
}),
}}
loading={loading}
/>

View File

@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import { useTranslation } from 'react-i18next';
import {
Banner,
@@ -23,65 +24,8 @@ import {
import { UserContext } from '../context/User/index.js';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
function renderQuotaType(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 1:
return (
<Tag color='teal' size='large'>
按次计费
</Tag>
);
case 0:
return (
<Tag color='violet' size='large'>
按量计费
</Tag>
);
default:
return '未知';
}
}
function renderAvailable(available) {
return available ? (
<Popover
content={
<div style={{ padding: 8 }}>您的分组可以使用该模型</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconVerify style={{ color: 'green' }} size="large" />
</Popover>
) : (
<Popover
content={
<div style={{ padding: 8 }}>您的分组无权使用该模型</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
</Popover>
);
}
const ModelPricing = () => {
const { t } = useTranslation();
const [filteredValue, setFilteredValue] = useState([]);
const compositionRef = useRef({ isComposition: false });
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
@@ -115,10 +59,68 @@ const ModelPricing = () => {
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
function renderQuotaType(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 1:
return (
<Tag color='teal' size='large'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' size='large'>
{t('按量计费')}
</Tag>
);
default:
return t('未知');
}
}
function renderAvailable(available) {
return available ? (
<Popover
content={
<div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconVerify style={{ color: 'green' }} size="large" />
</Popover>
) : (
<Popover
content={
<div style={{ padding: 8 }}>{t('您的分组无权使用该模型')}</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
</Popover>
);
}
const columns = [
{
title: '可用性',
title: t('可用性'),
dataIndex: 'available',
render: (text, record, index) => {
// if record.enable_groups contains selectedGroup, then available is true
@@ -127,20 +129,8 @@ const ModelPricing = () => {
sorter: (a, b) => a.available - b.available,
},
{
title: (
<Space>
<span>模型名称</span>
<Input
placeholder='模糊搜索'
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
</Space>
),
dataIndex: 'model_name', // 以finish_time作为dataIndex
title: t('模型名称'),
dataIndex: 'model_name',
render: (text, record, index) => {
return (
<>
@@ -161,7 +151,7 @@ const ModelPricing = () => {
filteredValue,
},
{
title: '计费类型',
title: t('计费类型'),
dataIndex: 'quota_type',
render: (text, record, index) => {
return renderQuotaType(parseInt(text));
@@ -169,7 +159,7 @@ const ModelPricing = () => {
sorter: (a, b) => a.quota_type - b.quota_type,
},
{
title: '可用分组',
title: t('可用分组'),
dataIndex: 'enable_groups',
render: (text, record, index) => {
// enable_groups is a string array
@@ -193,7 +183,10 @@ const ModelPricing = () => {
size='large'
onClick={() => {
setSelectedGroup(group);
showInfo('当前查看的分组为:' + group + ',倍率为:' + groupRatio[group]);
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
group: group,
ratio: groupRatio[group]
}));
}}
>
{group}
@@ -208,10 +201,13 @@ const ModelPricing = () => {
{
title: () => (
<span style={{'display':'flex','alignItems':'center'}}>
倍率
{t('倍率')}
<Popover
content={
<div style={{ padding: 8 }}>倍率是为了方便换算不同价格的模型<br/>点击查看倍率说明</div>
<div style={{ padding: 8 }}>
{t('倍率是为了方便换算不同价格的模型')}<br/>
{t('点击查看倍率说明')}
</div>
}
position='top'
style={{
@@ -237,18 +233,18 @@ const ModelPricing = () => {
let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
content = (
<>
<Text>模型{record.quota_type === 0 ? text : '无'}</Text>
<Text>{t('模型倍率')}{record.quota_type === 0 ? text : t('无')}</Text>
<br />
<Text>补全{record.quota_type === 0 ? completionRatio : '无'}</Text>
<Text>{t('补全倍率')}{record.quota_type === 0 ? completionRatio : t('无')}</Text>
<br />
<Text>分组{groupRatio[selectedGroup]}</Text>
<Text>{t('分组倍率')}{groupRatio[selectedGroup]}</Text>
</>
);
return <div>{content}</div>;
},
},
{
title: '模型价格',
title: t('模型价格'),
dataIndex: 'model_price',
render: (text, record, index) => {
let content = text;
@@ -261,14 +257,14 @@ const ModelPricing = () => {
groupRatio[selectedGroup];
content = (
<>
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
<Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
<br />
<Text>补全 ${completionRatioPrice} / 1M tokens</Text>
<Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
</>
);
} else {
let price = parseFloat(text) * groupRatio[selectedGroup];
content = <>模型价格${price}</>;
content = <>${t('模型价格')}${price}</>;
}
return <div>{content}</div>;
},
@@ -349,41 +345,62 @@ const ModelPricing = () => {
type="success"
fullMode={false}
closeIcon="null"
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
group: userState.user.group,
ratio: groupRatio[userState.user.group]
})}
/>
) : (
<Banner
type='warning'
fullMode={false}
closeIcon="null"
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
ratio: groupRatio['default']
})}
/>
)}
<br/>
<Banner
type="info"
fullMode={false}
description={<div>按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率/ 500000 单位美元</div>}
description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
closeIcon="null"
/>
<br/>
<Button
theme='light'
type='tertiary'
style={{width: 150}}
onClick={() => {
copyText(selectedRowKeys);
}}
disabled={selectedRowKeys == ""}
>
复制选中模型
</Button>
<Space style={{ marginBottom: 16 }}>
<Input
placeholder={t('模糊搜索模型名称')}
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
<Button
theme='light'
type='tertiary'
style={{width: 150}}
onClick={() => {
copyText(selectedRowKeys);
}}
disabled={selectedRowKeys == ""}
>
{t('复制选中模型')}
</Button>
</Space>
<Table
style={{ marginTop: 5 }}
columns={columns}
dataSource={models}
loading={loading}
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: models.length
}),
pageSize: models.length,
showSizeChanger: false,
}}

View File

@@ -2,8 +2,10 @@ import React, { useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked';
import { useTranslation } from 'react-i18next';
const OtherSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
Notice: '',
SystemName: '',
@@ -54,10 +56,10 @@ const OtherSetting = () => {
try {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true }));
await updateOption('Notice', inputs.Notice);
showSuccess('公告已更新');
showSuccess(t('公告已更新'));
} catch (error) {
console.error('公告更新失败', error);
showError('公告更新失败');
console.error(t('公告更新失败'), error);
showError(t('公告更新失败'));
} finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
}
@@ -72,10 +74,10 @@ const OtherSetting = () => {
SystemName: true,
}));
await updateOption('SystemName', inputs.SystemName);
showSuccess('系统名称已更新');
showSuccess(t('系统名称已更新'));
} catch (error) {
console.error('系统名称更新失败', error);
showError('系统名称更新失败');
console.error(t('系统名称更新失败'), error);
showError(t('系统名称更新失败'));
} finally {
setLoadingInput((loadingInput) => ({
...loadingInput,
@@ -193,17 +195,17 @@ const OtherSetting = () => {
getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'通用设置'}>
<Form.Section text={t('通用设置')}>
<Form.TextArea
label={'公告'}
placeholder={'在此输入新的公告内容,支持 Markdown & HTML 代码'}
label={t('公告')}
placeholder={t('在此输入新的公告内容,支持 Markdown & HTML 代码')}
field={'Notice'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
/>
<Button onClick={submitNotice} loading={loadingInput['Notice']}>
设置公告
{t('设置公告')}
</Button>
</Form.Section>
</Form>
@@ -213,10 +215,10 @@ const OtherSetting = () => {
getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'个性化设置'}>
<Form.Section text={t('个性化设置')}>
<Form.Input
label={'系统名称'}
placeholder={'在此输入系统名称'}
label={t('系统名称')}
placeholder={t('在此输入系统名称')}
field={'SystemName'}
onChange={handleInputChange}
/>
@@ -224,22 +226,20 @@ const OtherSetting = () => {
onClick={submitSystemName}
loading={loadingInput['SystemName']}
>
设置系统名称
{t('设置系统名称')}
</Button>
<Form.Input
label={'Logo 图片地址'}
placeholder={'在此输入 Logo 图片地址'}
label={t('Logo 图片地址')}
placeholder={t('在此输入 Logo 图片地址')}
field={'Logo'}
onChange={handleInputChange}
/>
<Button onClick={submitLogo} loading={loadingInput['Logo']}>
设置 Logo
{t('设置 Logo')}
</Button>
<Form.TextArea
label={'首页内容'}
placeholder={
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
}
label={t('首页内容')}
placeholder={t('在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页')}
field={'HomePageContent'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
@@ -249,39 +249,35 @@ const OtherSetting = () => {
onClick={() => submitOption('HomePageContent')}
loading={loadingInput['HomePageContent']}
>
设置首页内容
{t('设置首页内容')}
</Button>
<Form.TextArea
label={'关于'}
placeholder={
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
}
label={t('关于')}
placeholder={t('在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面')}
field={'About'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
/>
<Button onClick={submitAbout} loading={loadingInput['About']}>
设置关于
{t('设置关于')}
</Button>
{/* */}
<Banner
fullMode={false}
type='info'
description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目'
description={t('移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目')}
closeIcon={null}
style={{ marginTop: 15 }}
/>
<Form.Input
label={'页脚'}
placeholder={
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
}
label={t('页脚')}
placeholder={t('在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码')}
field={'Footer'}
onChange={handleInputChange}
/>
<Button onClick={submitFooter} loading={loadingInput['Footer']}>
设置页脚
{t('设置页脚')}
</Button>
</Form.Section>
</Form>

View File

@@ -6,11 +6,13 @@ import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify';
import React, { useContext } from 'react';
import { StyleContext } from '../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const { Sider, Content, Header, Footer } = Layout;
const PageLayout = () => {
const [styleState, styleDispatch] = useContext(StyleContext);
const { t } = useTranslation();
return (
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
@@ -28,7 +30,7 @@ const PageLayout = () => {
<App />
</Content>
<Layout.Footer>
<FooterBar></FooterBar>
<FooterBar />
</Layout.Footer>
</Layout>
</Layout>

View File

@@ -33,10 +33,12 @@ import {
stringToColor,
} from '../helpers/render';
import TelegramLoginButton from 'react-telegram-login';
import { useTranslation } from 'react-i18next';
const PersonalSetting = () => {
const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate();
const { t } = useTranslation();
const [inputs, setInputs] = useState({
wechat_verification_code: '',
@@ -110,7 +112,7 @@ const PersonalSetting = () => {
if (success) {
setSystemToken(data);
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板`);
showSuccess(t('令牌已重置并已复制到剪贴板'));
} else {
showError(message);
}
@@ -151,18 +153,18 @@ const PersonalSetting = () => {
const handleAffLinkClick = async (e) => {
e.target.select();
await copy(e.target.value);
showSuccess(`邀请链接已复制到剪切板`);
showSuccess(t('邀请链接已复制到剪切板'));
};
const handleSystemTokenClick = async (e) => {
e.target.select();
await copy(e.target.value);
showSuccess(`系统令牌已复制到剪切板`);
showSuccess(t('系统令牌已复制到剪切板'));
};
const deleteAccount = async () => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
showError('请输入你的账户名以确认删除!');
showError(t('请输入你的账户名以确认删除!'));
return;
}
@@ -170,7 +172,7 @@ const PersonalSetting = () => {
const {success, message} = res.data;
if (success) {
showSuccess('账户已删除!');
showSuccess(t('账户已删除!'));
await API.get('/api/user/logout');
userDispatch({type: 'logout'});
localStorage.removeItem('user');
@@ -187,7 +189,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('微信账户绑定成功!');
showSuccess(t('微信账户绑定成功!'));
setShowWeChatBindModal(false);
} else {
showError(message);
@@ -196,7 +198,7 @@ const PersonalSetting = () => {
const changePassword = async () => {
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
showError('两次输入的密码不一致!');
showError(t('两次输入的密码不一致!'));
return;
}
const res = await API.put(`/api/user/self`, {
@@ -204,7 +206,7 @@ const PersonalSetting = () => {
});
const {success, message} = res.data;
if (success) {
showSuccess('密码修改成功!');
showSuccess(t('密码修改成功!'));
setShowWeChatBindModal(false);
} else {
showError(message);
@@ -214,7 +216,7 @@ const PersonalSetting = () => {
const transfer = async () => {
if (transferAmount < getQuotaPerUnit()) {
showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
return;
}
const res = await API.post(`/api/user/aff_transfer`, {
@@ -232,7 +234,7 @@ const PersonalSetting = () => {
const sendVerificationCode = async () => {
if (inputs.email === '') {
showError('请输入邮箱!');
showError(t('请输入邮箱!'));
return;
}
setDisableButton(true);
@@ -246,7 +248,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('验证码发送成功,请检查邮箱!');
showSuccess(t('验证码发送成功,请检查邮箱!'));
} else {
showError(message);
}
@@ -255,7 +257,7 @@ const PersonalSetting = () => {
const bindEmail = async () => {
if (inputs.email_verification_code === '') {
showError('请输入邮箱验证码!');
showError(t('请输入邮箱验证码!'));
return;
}
setLoading(true);
@@ -264,7 +266,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('邮箱账户绑定成功!');
showSuccess(t('邮箱账户绑定成功!'));
setShowEmailBindModal(false);
userState.user.email = inputs.email;
} else {
@@ -299,7 +301,7 @@ const PersonalSetting = () => {
<Layout>
<Layout.Content>
<Modal
title='请输入要划转的数量'
title={t('请输入要划转的数量')}
visible={openTransfer}
onOk={transfer}
onCancel={handleCancel}
@@ -308,7 +310,7 @@ const PersonalSetting = () => {
centered={true}
>
<div style={{marginTop: 20}}>
<Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
<Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
<Input
style={{marginTop: 5}}
value={userState?.user?.aff_quota}
@@ -317,8 +319,7 @@ const PersonalSetting = () => {
</div>
<div style={{marginTop: 20}}>
<Typography.Text>
{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` +
renderQuota(getQuotaPerUnit())}
{t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text>
<div>
<InputNumber
@@ -348,9 +349,9 @@ const PersonalSetting = () => {
title={<Typography.Text>{getUsername()}</Typography.Text>}
description={
isRoot() ? (
<Tag color='red'>管理员</Tag>
<Tag color='red'>{t('管理员')}</Tag>
) : (
<Tag color='blue'>普通用户</Tag>
<Tag color='blue'>{t('普通用户')}</Tag>
)
}
></Card.Meta>
@@ -365,13 +366,13 @@ const PersonalSetting = () => {
}
>
<Descriptions row>
<Descriptions.Item itemKey='当前余额'>
<Descriptions.Item itemKey={t('当前余额')}>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
<Descriptions.Item itemKey={t('历史消耗')}>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
<Descriptions.Item itemKey={t('请求次数')}>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
@@ -380,7 +381,7 @@ const PersonalSetting = () => {
style={{marginTop: 10}}
footer={
<div>
<Typography.Text>邀请链接</Typography.Text>
<Typography.Text>{t('邀请链接')}</Typography.Text>
<Input
style={{marginTop: 10}}
value={affLink}
@@ -390,35 +391,35 @@ const PersonalSetting = () => {
</div>
}
>
<Typography.Title heading={6}>邀请信息</Typography.Title>
<Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
<div style={{marginTop: 10}}>
<Descriptions row>
<Descriptions.Item itemKey='待使用收益'>
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
{renderQuota(userState?.user?.aff_quota)}
</span>
<Descriptions.Item itemKey={t('待使用收益')}>
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
{renderQuota(userState?.user?.aff_quota)}
</span>
<Button
type={'secondary'}
onClick={() => setOpenTransfer(true)}
size={'small'}
style={{marginLeft: 10}}
>
划转
{t('划转')}
</Button>
</Descriptions.Item>
<Descriptions.Item itemKey='总收益'>
<Descriptions.Item itemKey={t('总收益')}>
{renderQuota(userState?.user?.aff_history_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='邀请人数'>
<Descriptions.Item itemKey={t('邀请人数')}>
{userState?.user?.aff_count}
</Descriptions.Item>
</Descriptions>
</div>
</Card>
<Card style={{marginTop: 10}}>
<Typography.Title heading={6}>个人信息</Typography.Title>
<Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
<div style={{marginTop: 20}}>
<Typography.Text strong>邮箱</Typography.Text>
<Typography.Text strong>{t('邮箱')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -427,7 +428,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.email !== ''
? userState.user.email
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -439,14 +440,14 @@ const PersonalSetting = () => {
}}
>
{userState.user && userState.user.email !== ''
? '修改绑定'
: '绑定邮箱'}
? t('修改绑定')
: t('绑定邮箱')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>微信</Typography.Text>
<Typography.Text strong>{t('微信')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -454,8 +455,8 @@ const PersonalSetting = () => {
<Input
value={
userState.user && userState.user.wechat_id !== ''
? '已绑定'
: '未绑定'
? t('已绑定')
: t('未绑定')
}
readonly={true}
></Input>
@@ -467,13 +468,13 @@ const PersonalSetting = () => {
!status.wechat_login
}
>
{status.wechat_login ? '绑定' : '未启用'}
{status.wechat_login ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>GitHub</Typography.Text>
<Typography.Text strong>{t('GitHub')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -482,7 +483,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.github_id !== ''
? userState.user.github_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -497,13 +498,13 @@ const PersonalSetting = () => {
!status.github_oauth
}
>
{status.github_oauth ? '绑定' : '未启用'}
{status.github_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>Telegram</Typography.Text>
<Typography.Text strong>{t('Telegram')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -512,7 +513,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.telegram_id !== ''
? userState.user.telegram_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -520,7 +521,7 @@ const PersonalSetting = () => {
<div>
{status.telegram_oauth ? (
userState.user.telegram_id !== '' ? (
<Button disabled={true}>已绑定</Button>
<Button disabled={true}>{t('已绑定')}</Button>
) : (
<TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind'
@@ -528,13 +529,13 @@ const PersonalSetting = () => {
/>
)
) : (
<Button disabled={true}>未启用</Button>
<Button disabled={true}>{t('未启用')}</Button>
)}
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>LinuxDO</Typography.Text>
<Typography.Text strong>{t('LinuxDO')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -543,7 +544,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.linux_do_id !== ''
? userState.user.linux_do_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -558,7 +559,7 @@ const PersonalSetting = () => {
!status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? '绑定' : '未启用'}
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
@@ -566,14 +567,14 @@ const PersonalSetting = () => {
<div style={{marginTop: 10}}>
<Space>
<Button onClick={generateAccessToken}>
生成系统访问令牌
{t('生成系统访问令牌')}
</Button>
<Button
onClick={() => {
setShowChangePasswordModal(true);
}}
>
修改密码
{t('修改密码')}
</Button>
<Button
type={'danger'}
@@ -581,7 +582,7 @@ const PersonalSetting = () => {
setShowAccountDeleteModal(true);
}}
>
删除个人账户
{t('删除个人账户')}
</Button>
</Space>
@@ -599,7 +600,7 @@ const PersonalSetting = () => {
setShowWeChatBindModal(true);
}}
>
绑定微信账号
{t('绑定微信账号')}
</Button>
)}
<Modal
@@ -623,7 +624,7 @@ const PersonalSetting = () => {
}
/>
<Button color='' fluid size='large' onClick={bindWeChat}>
绑定
{t('绑定')}
</Button>
</Modal>
</div>
@@ -637,7 +638,7 @@ const PersonalSetting = () => {
centered={true}
maskClosable={false}
>
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
<Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
<div
style={{
marginTop: 20,
@@ -729,7 +730,7 @@ const PersonalSetting = () => {
<div style={{marginTop: 20}}>
<Input
name='set_new_password'
placeholder='新密码'
placeholder={t('新密码')}
value={inputs.set_new_password}
onChange={(value) =>
handleInputChange('set_new_password', value)
@@ -738,7 +739,7 @@ const PersonalSetting = () => {
<Input
style={{marginTop: 20}}
name='set_new_password_confirmation'
placeholder='确认新密码'
placeholder={t('确认新密码')}
value={inputs.set_new_password_confirmation}
onChange={(value) =>
handleInputChange('set_new_password_confirmation', value)

View File

@@ -19,55 +19,55 @@ import {
Tag,
} from '@douyinfe/semi-ui';
import EditRedemption from '../pages/Redemption/EditRedemption';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
function renderStatus(status) {
switch (status) {
case 1:
return (
<Tag color='green' size='large'>
未使用
</Tag>
);
case 2:
return (
<Tag color='red' size='large'>
{' '}
已禁用{' '}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large'>
{' '}
已使用{' '}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{' '}
未知状态{' '}
</Tag>
);
}
}
const RedemptionsTable = () => {
const { t } = useTranslation();
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag color='green' size='large'>
{t('未使用')}
</Tag>
);
case 2:
return (
<Tag color='red' size='large'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large'>
{t('已使用')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{t('未知状态')}
</Tag>
);
}
};
const columns = [
{
title: 'ID',
title: t('ID'),
dataIndex: 'id',
},
{
title: '名称',
title: t('名称'),
dataIndex: 'name',
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
@@ -75,24 +75,24 @@ const RedemptionsTable = () => {
},
},
{
title: '额度',
title: t('额度'),
dataIndex: 'quota',
render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>;
},
},
{
title: '创建时间',
title: t('创建时间'),
dataIndex: 'created_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text)}</div>;
},
},
{
title: '兑换人ID',
title: t('兑换人ID'),
dataIndex: 'used_user_id',
render: (text, record, index) => {
return <div>{text === 0 ? '无' : text}</div>;
return <div>{text === 0 ? t('无') : text}</div>;
},
},
{
@@ -102,7 +102,7 @@ const RedemptionsTable = () => {
<div>
<Popover content={record.key} style={{ padding: 20 }} position='top'>
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
查看
{t('查看')}
</Button>
</Popover>
<Button
@@ -113,11 +113,11 @@ const RedemptionsTable = () => {
await copyText(record.key);
}}
>
复制
{t('复制')}
</Button>
<Popconfirm
title='确定是否要删除此兑换码?'
content='此修改将不可逆'
title={t('确定是否要删除此兑换码?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -127,7 +127,7 @@ const RedemptionsTable = () => {
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
删除
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -139,7 +139,7 @@ const RedemptionsTable = () => {
manageRedemption(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -151,7 +151,7 @@ const RedemptionsTable = () => {
}}
disabled={record.status === 3}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -164,7 +164,7 @@ const RedemptionsTable = () => {
}}
disabled={record.status !== 1}
>
编辑
{t('编辑')}
</Button>
</div>
),
@@ -239,10 +239,10 @@ const RedemptionsTable = () => {
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制到剪贴板!');
showSuccess(t('已复制到剪贴板!'));
} else {
// setSearchKeyword(text);
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
}
};
@@ -286,7 +286,7 @@ const RedemptionsTable = () => {
}
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
let redemption = res.data.data;
let newRedemptions = [...redemptions];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
@@ -381,11 +381,11 @@ const RedemptionsTable = () => {
></EditRedemption>
<Form onSubmit={searchRedemptions}>
<Form.Input
label='搜索关键字'
label={t('搜索关键字')}
field='keyword'
icon='search'
iconPosition='left'
placeholder='关键字(id或者名称)'
placeholder={t('关键字(id或者名称)')}
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
@@ -404,14 +404,14 @@ const RedemptionsTable = () => {
setShowEdit(true);
}}
>
添加兑换码
{t('添加兑换码')}
</Button>
<Button
label='复制所选兑换码'
label={t('复制所选兑换码')}
type='warning'
onClick={async () => {
if (selectedKeys.length === 0) {
showError('请至少选择一个兑换码!');
showError(t('请至少选择一个兑换码!'));
return;
}
let keys = '';
@@ -421,7 +421,7 @@ const RedemptionsTable = () => {
await copyText(keys);
}}
>
复制所选兑换码到剪贴板
{t('复制所选兑换码到剪贴板')}
</Button>
</div>
@@ -436,7 +436,11 @@ const RedemptionsTable = () => {
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) =>
`${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length}`,
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: redemptions.length
}),
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);

View File

@@ -12,8 +12,10 @@ import WeChatIcon from './WeChatIcon.js';
import TelegramLoginButton from 'react-telegram-login/src';
import { setUserData } from '../helpers/data.js';
import { UserContext } from '../context/User/index.js';
import { useTranslation } from 'react-i18next';
const RegisterForm = () => {
const { t } = useTranslation();
const [inputs, setInputs] = useState({
username: '',
password: '',
@@ -182,28 +184,28 @@ const RegisterForm = () => {
<div style={{ width: 500 }}>
<Card>
<Title heading={2} style={{ textAlign: 'center' }}>
新用户注册
{t('新用户注册')}
</Title>
<Form size="large">
<Form.Input
field={'username'}
label={'用户名'}
placeholder="用户名"
label={t('用户名')}
placeholder={t('用户名')}
name="username"
onChange={(value) => handleChange('username', value)}
/>
<Form.Input
field={'password'}
label={'密码'}
placeholder="密码,最短 8 位,最长 20 位"
label={t('密码')}
placeholder={t('输入密码,最短 8 位,最长 20 位')}
name="password"
type="password"
onChange={(value) => handleChange('password', value)}
/>
<Form.Input
field={'password2'}
label={'确认密码'}
placeholder="确认密码"
label={t('确认密码')}
placeholder={t('确认密码')}
name="password2"
type="password"
onChange={(value) => handleChange('password2', value)}
@@ -212,21 +214,21 @@ const RegisterForm = () => {
<>
<Form.Input
field={'email'}
label={'邮箱'}
placeholder="输入邮箱地址"
label={t('邮箱')}
placeholder={t('输入邮箱地址')}
onChange={(value) => handleChange('email', value)}
name="email"
type="email"
suffix={
<Button onClick={sendVerificationCode} disabled={loading}>
获取验证码
{t('获取验证码')}
</Button>
}
/>
<Form.Input
field={'verification_code'}
label={'验证码'}
placeholder="输入验证码"
label={t('验证码')}
placeholder={t('输入验证码')}
onChange={(value) => handleChange('verification_code', value)}
name="verification_code"
/>
@@ -242,7 +244,7 @@ const RegisterForm = () => {
htmlType={'submit'}
onClick={handleSubmit}
>
注册
{t('注册')}
</Button>
</Form>
<div
@@ -253,9 +255,9 @@ const RegisterForm = () => {
}}
>
<Text>
已有账户
{t('已有账户?')}
<Link to="/login">
点击登录
{t('点击登录')}
</Link>
</Text>
</div>
@@ -265,7 +267,7 @@ const RegisterForm = () => {
status.linuxdo_oauth ? (
<>
<Divider margin='12px' align='center'>
第三方登录
{t('第三方登录')}
</Divider>
<div
style={{
@@ -330,12 +332,12 @@ const RegisterForm = () => {
)}
</Card>
<Modal
title='微信扫码登录'
title={t('微信扫码登录')}
visible={showWeChatLoginModal}
maskClosable={true}
onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)}
okText={'登录'}
okText={t('登录')}
size={'small'}
centered={true}
>
@@ -350,14 +352,14 @@ const RegisterForm = () => {
</div>
<div style={{ textAlign: 'center' }}>
<p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p>
</div>
<Form size='large'>
<Form.Input
field={'wechat_verification_code'}
placeholder='验证码'
label={'验证码'}
placeholder={t('验证码')}
label={t('验证码')}
value={inputs.wechat_verification_code}
onChange={(value) =>
handleChange('wechat_verification_code', value)

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { StatusContext } from '../context/Status';
import { useTranslation } from 'react-i18next';
import {
API,
@@ -36,6 +37,7 @@ import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
const SiderBar = () => {
const { t } = useTranslation();
const [styleState, styleDispatch] = useContext(StyleContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const defaultIsCollapsed =
@@ -74,30 +76,26 @@ const SiderBar = () => {
icon: <IconCommentStroked />,
},
{
text: '渠道',
text: t('渠道'),
itemKey: 'channel',
to: '/channel',
icon: <IconLayers />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '聊天',
text: t('聊天'),
itemKey: 'chat',
// to: '/chat',
items: chatItems,
icon: <IconComment />,
// className: localStorage.getItem('chat_link')
// ? 'semi-navigation-item-normal'
// : 'tableHiddle',
},
{
text: '令牌',
text: t('令牌'),
itemKey: 'token',
to: '/token',
icon: <IconKey />,
},
{
text: '数据看板',
text: t('数据看板'),
itemKey: 'detail',
to: '/detail',
icon: <IconCalendarClock />,
@@ -107,33 +105,33 @@ const SiderBar = () => {
: 'tableHiddle',
},
{
text: '兑换码',
text: t('兑换码'),
itemKey: 'redemption',
to: '/redemption',
icon: <IconGift />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '钱包',
text: t('钱包'),
itemKey: 'topup',
to: '/topup',
icon: <IconCreditCard />,
},
{
text: '用户管理',
text: t('用户管理'),
itemKey: 'user',
to: '/user',
icon: <IconUser />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '日志',
text: t('日志'),
itemKey: 'log',
to: '/log',
icon: <IconHistogram />,
},
{
text: '绘图',
text: t('绘图'),
itemKey: 'midjourney',
to: '/midjourney',
icon: <IconImage />,
@@ -143,7 +141,7 @@ const SiderBar = () => {
: 'tableHiddle',
},
{
text: '异步任务',
text: t('异步任务'),
itemKey: 'task',
to: '/task',
icon: <IconChecklistStroked />,
@@ -153,24 +151,20 @@ const SiderBar = () => {
: 'tableHiddle',
},
{
text: '设置',
text: t('设置'),
itemKey: 'setting',
to: '/setting',
icon: <IconSetting />,
},
// {
// text: '关于',
// itemKey: 'about',
// to: '/about',
// icon: <IconAt/>
// }
],
[
localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_drawing'),
localStorage.getItem('enable_task'),
localStorage.getItem('chat_link'), chatItems,
localStorage.getItem('chat_link'),
chatItems,
isAdmin(),
t,
],
);

View File

@@ -23,67 +23,66 @@ import {
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
import EditToken from '../pages/Token/EditToken';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
function renderStatus(status, model_limits_enabled = false) {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
已启用限制模型
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
已启用
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
{' '}
已禁用{' '}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
{' '}
已过期{' '}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
{' '}
已耗尽{' '}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{' '}
未知状态{' '}
</Tag>
);
}
}
const TokensTable = () => {
const { t } = useTranslation();
const renderStatus = (status, model_limits_enabled = false) => {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{t('未知状态')}
</Tag>
);
}
};
const columns = [
{
title: '名称',
title: t('名称'),
dataIndex: 'name',
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
@@ -96,21 +95,21 @@ const TokensTable = () => {
},
},
{
title: '已用额度',
title: t('已用额度'),
dataIndex: 'used_quota',
render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>;
},
},
{
title: '剩余额度',
title: t('剩余额度'),
dataIndex: 'remain_quota',
render: (text, record, index) => {
return (
<div>
{record.unlimited_quota ? (
<Tag size={'large'} color={'white'}>
无限制
{t('无限制')}
</Tag>
) : (
<Tag size={'large'} color={'light-blue'}>
@@ -122,19 +121,19 @@ const TokensTable = () => {
},
},
{
title: '创建时间',
title: t('创建时间'),
dataIndex: 'created_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text)}</div>;
},
},
{
title: '过期时间',
title: t('过期时间'),
dataIndex: 'expired_time',
render: (text, record, index) => {
return (
<div>
{record.expired_time === -1 ? '永不过期' : renderTimestamp(text)}
{record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
</div>
);
},
@@ -199,7 +198,7 @@ const TokensTable = () => {
} catch (e) {
console.log(e);
showError('聊天链接配置错误,请联系管理员');
showError(t('聊天链接配置错误,请联系管理员'));
}
}
return (
@@ -210,7 +209,7 @@ const TokensTable = () => {
position='top'
>
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
查看
{t('查看')}
</Button>
</Popover>
<Button
@@ -221,24 +220,24 @@ const TokensTable = () => {
await copyText('sk-' + record.key);
}}
>
复制
{t('复制')}
</Button>
<SplitButtonGroup
style={{ marginRight: 1 }}
aria-label='项目操作按钮组'
aria-label={t('项目操作按钮组')}
>
<Button
theme='light'
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
onClick={() => {
if (chatsArray.length === 0) {
showError('请联系管理员配置聊天链接');
showError(t('请联系管理员配置聊天链接'));
} else {
onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record);
}
}}
>
聊天
{t('聊天')}
</Button>
<Dropdown
trigger='click'
@@ -256,8 +255,8 @@ const TokensTable = () => {
</Dropdown>
</SplitButtonGroup>
<Popconfirm
title='确定是否要删除此令牌?'
content='此修改将不可逆'
title={t('确定是否要删除此令牌?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -267,7 +266,7 @@ const TokensTable = () => {
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
删除
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -279,7 +278,7 @@ const TokensTable = () => {
manageToken(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -290,7 +289,7 @@ const TokensTable = () => {
manageToken(record.id, 'enable', record);
}}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -302,7 +301,7 @@ const TokensTable = () => {
setShowEdit(true);
}}
>
编辑
{t('编辑')}
</Button>
</div>
);
@@ -371,10 +370,10 @@ const TokensTable = () => {
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制到剪贴板!');
showSuccess(t('已复制到剪贴板!'));
} else {
Modal.error({
title: '无法复制到剪贴板,请手动复制',
title: t('无法复制到剪贴板,请手动复制'),
content: text,
size: 'large',
});
@@ -395,37 +394,6 @@ const TokensTable = () => {
let encodedServerAddress = encodeURIComponent(serverAddress);
url = url.replaceAll('{address}', encodedServerAddress);
url = url.replaceAll('{key}', 'sk-' + record.key);
// console.log(url);
// const chatLink = localStorage.getItem('chat_link');
// const mjLink = localStorage.getItem('chat_link2');
// let defaultUrl;
//
// if (chatLink) {
// defaultUrl =
// chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
// }
// let url;
// switch (type) {
// case 'ama':
// url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
// break;
// case 'opencat':
// url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
// break;
// case 'lobe':
// url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
// break;
// case 'next-mj':
// url =
// mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
// break;
// default:
// if (!chatLink) {
// showError('管理员未设置聊天链接');
// return;
// }
// url = defaultUrl;
// }
window.open(url, '_blank');
};
@@ -571,29 +539,29 @@ const TokensTable = () => {
>
<Form.Input
field='keyword'
label='搜索关键字'
placeholder='令牌名称'
label={t('搜索关键字')}
placeholder={t('令牌名称')}
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
/>
<Form.Input
field='token'
label='Key'
placeholder='密钥'
label={t('密钥')}
placeholder={t('密钥')}
value={searchToken}
loading={searching}
onChange={handleSearchTokenChange}
/>
<Button
label='查询'
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={searchTokens}
style={{ marginRight: 8 }}
>
查询
{t('查询')}
</Button>
</Form>
<Divider style={{margin:'15px 0'}}/>
@@ -609,14 +577,14 @@ const TokensTable = () => {
setShowEdit(true);
}}
>
添加令牌
{t('添加令牌')}
</Button>
<Button
label='复制所选令牌'
label={t('复制所选令牌')}
type='warning'
onClick={async () => {
if (selectedKeys.length === 0) {
showError('请至少选择一个令牌!');
showError(t('请至少选择一个令牌!'));
return;
}
let keys = '';
@@ -627,7 +595,7 @@ const TokensTable = () => {
await copyText(keys);
}}
>
复制所选令牌到剪贴板
{t('复制所选令牌到剪贴板')}
</Button>
</div>
@@ -642,7 +610,11 @@ const TokensTable = () => {
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) =>
`${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length}`,
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: tokens.length
}),
onPageSizeChange: (size) => {
setPageSize(size);
setActivePage(1);

View File

@@ -13,67 +13,69 @@ import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
import AddUser from '../pages/User/AddUser';
import EditUser from '../pages/User/EditUser';
function renderRole(role) {
switch (role) {
case 1:
return <Tag size='large'>普通用户</Tag>;
case 10:
return (
<Tag color='yellow' size='large'>
管理员
</Tag>
);
case 100:
return (
<Tag color='orange' size='large'>
超级管理员
</Tag>
);
default:
return (
<Tag color='red' size='large'>
未知身份
</Tag>
);
}
}
import { useTranslation } from 'react-i18next';
const UsersTable = () => {
const { t } = useTranslation();
function renderRole(role) {
switch (role) {
case 1:
return <Tag size='large'>{t('普通用户')}</Tag>;
case 10:
return (
<Tag color='yellow' size='large'>
{t('管理员')}
</Tag>
);
case 100:
return (
<Tag color='orange' size='large'>
{t('超级管理员')}
</Tag>
);
default:
return (
<Tag color='red' size='large'>
{t('未知身份')}
</Tag>
);
}
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
},
{
title: '用户名',
title: t('用户名'),
dataIndex: 'username',
},
{
title: '分组',
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
return <div>{renderGroup(text)}</div>;
},
},
{
title: '统计信息',
title: t('统计信息'),
dataIndex: 'info',
render: (text, record, index) => {
return (
<div>
<Space spacing={1}>
<Tooltip content={'剩余额度'}>
<Tooltip content={t('剩余额度')}>
<Tag color='white' size='large'>
{renderQuota(record.quota)}
</Tag>
</Tooltip>
<Tooltip content={'已用额度'}>
<Tooltip content={t('已用额度')}>
<Tag color='white' size='large'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
<Tooltip content={'调用次数'}>
<Tooltip content={t('调用次数')}>
<Tag color='white' size='large'>
{renderNumber(record.request_count)}
</Tag>
@@ -84,26 +86,26 @@ const UsersTable = () => {
},
},
{
title: '邀请信息',
title: t('邀请信息'),
dataIndex: 'invite',
render: (text, record, index) => {
return (
<div>
<Space spacing={1}>
<Tooltip content={'邀请人数'}>
<Tooltip content={t('邀请人数')}>
<Tag color='white' size='large'>
{renderNumber(record.aff_count)}
</Tag>
</Tooltip>
<Tooltip content={'邀请总收益'}>
<Tooltip content={t('邀请总收益')}>
<Tag color='white' size='large'>
{renderQuota(record.aff_history_quota)}
</Tag>
</Tooltip>
<Tooltip content={'邀请人ID'}>
<Tooltip content={t('邀请人ID')}>
{record.inviter_id === 0 ? (
<Tag color='white' size='large'>
{t('无')}
</Tag>
) : (
<Tag color='white' size='large'>
@@ -117,20 +119,20 @@ const UsersTable = () => {
},
},
{
title: '角色',
title: t('角色'),
dataIndex: 'role',
render: (text, record, index) => {
return <div>{renderRole(text)}</div>;
},
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
render: (text, record, index) => {
return (
<div>
{record.DeletedAt !== null ? (
<Tag color='red'>已注销</Tag>
<Tag color='red'>{t('已注销')}</Tag>
) : (
renderStatus(text)
)}
@@ -148,29 +150,25 @@ const UsersTable = () => {
) : (
<>
<Popconfirm
title='确定?'
title={t('确定?')}
okType={'warning'}
onConfirm={() => {
manageUser(record.id, 'promote', record);
}}
>
<Button theme='light' type='warning' style={{ marginRight: 1 }}>
提升
{t('提升')}
</Button>
</Popconfirm>
<Popconfirm
title='确定?'
title={t('确定?')}
okType={'warning'}
onConfirm={() => {
manageUser(record.id, 'demote', record);
}}
>
<Button
theme='light'
type='secondary'
style={{ marginRight: 1 }}
>
降级
<Button theme='light' type='secondary' style={{ marginRight: 1 }}>
{t('降级')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -182,7 +180,7 @@ const UsersTable = () => {
manageUser(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -194,7 +192,7 @@ const UsersTable = () => {
}}
disabled={record.status === 3}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -206,11 +204,11 @@ const UsersTable = () => {
setShowEditUser(true);
}}
>
编辑
{t('编辑')}
</Button>
<Popconfirm
title='确定是否要注销此用户?'
content='相当于删除用户,此修改将不可逆'
title={t('确定是否要注销此用户?')}
content={t('相当于删除用户,此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -220,7 +218,7 @@ const UsersTable = () => {
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
注销
{t('注销')}
</Button>
</Popconfirm>
</>
@@ -327,17 +325,17 @@ const UsersTable = () => {
const renderStatus = (status) => {
switch (status) {
case 1:
return <Tag size='large'>已激活</Tag>;
return <Tag size='large'>{t('已激活')}</Tag>;
case 2:
return (
<Tag size='large' color='red'>
已封禁
{t('已封禁')}
</Tag>
);
default:
return (
<Tag size='large' color='grey'>
未知状态
{t('未知状态')}
</Tag>
);
}
@@ -452,41 +450,41 @@ const UsersTable = () => {
>
<div style={{ display: 'flex' }}>
<Space>
<Form.Input
label='搜索关键字'
icon='search'
field='keyword'
iconPosition='left'
placeholder='搜索用户的 ID用户名显示名称以及邮箱地址 ...'
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
<Form.Select
field='group'
label='分组'
optionList={groupOptions}
onChange={(value) => {
setSearchGroup(value);
searchUsers(searchKeyword, value);
}}
/>
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
>
查询
</Button>
<Form.Input
label={t('搜索关键字')}
icon='search'
field='keyword'
iconPosition='left'
placeholder={t('搜索用户的 ID用户名显示名称以及邮箱地址 ...')}
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
<Form.Select
field='group'
label={t('分组')}
optionList={groupOptions}
onChange={(value) => {
setSearchGroup(value);
searchUsers(searchKeyword, value);
}}
/>
<Button
theme='light'
type='primary'
onClick={() => {
setShowAddUser(true);
}}
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
>
添加用户
{t('查询')}
</Button>
<Button
theme='light'
type='primary'
onClick={() => {
setShowAddUser(true);
}}
>
{t('添加用户')}
</Button>
</Space>
</div>
@@ -496,6 +494,12 @@ const UsersTable = () => {
columns={columns}
dataSource={pageData}
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: users.length
}),
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
total: userCount,