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

@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
API,
isMobile,
@@ -61,6 +62,7 @@ function type2secretPrompt(type) {
}
const EditChannel = (props) => {
const { t } = useTranslation();
const navigate = useNavigate();
const channelId = props.editingChannel.id;
const isEdit = channelId !== undefined;
@@ -192,7 +194,7 @@ const EditChannel = (props) => {
const fetchUpstreamModelList = async (name) => {
if (inputs['type'] !== 1) {
showError('仅支持 OpenAI 接口格式');
showError(t('仅支持 OpenAI 接口格式'));
return;
}
setLoading(true);
@@ -207,7 +209,7 @@ const EditChannel = (props) => {
}
} else {
if (!inputs?.['key']) {
showError('请填写密钥');
showError(t('请填写密钥'));
err = true;
} else {
try {
@@ -232,9 +234,9 @@ const EditChannel = (props) => {
}
if (!err) {
handleInputChange(name, Array.from(new Set(models)));
showSuccess('获取模型列表成功');
showSuccess(t('获取模型列表成功'));
} else {
showError('获取模型列表失败');
showError(t('获取模型列表失败'));
}
setLoading(false);
};
@@ -305,15 +307,15 @@ const EditChannel = (props) => {
const submit = async () => {
if (!isEdit && (inputs.name === '' || inputs.key === '')) {
showInfo('请填写渠道名称和渠道密钥!');
showInfo(t('请填写渠道名称和渠道密钥!'));
return;
}
if (inputs.models.length === 0) {
showInfo('请至少选择一个模型!');
showInfo(t('请至少选择一个模型!'));
return;
}
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
showInfo('模型映射必须是合法的 JSON 格式!');
showInfo(t('模型映射必须是合法的 JSON 格式!'));
return;
}
let localInputs = { ...inputs };
@@ -331,7 +333,7 @@ const EditChannel = (props) => {
}
let res;
if (!Array.isArray(localInputs.models)) {
showError('提交失败,请勿重复提交!');
showError(t('提交失败,请勿重复提交!'));
handleCancel();
return;
}
@@ -349,9 +351,9 @@ const EditChannel = (props) => {
const { success, message } = res.data;
if (success) {
if (isEdit) {
showSuccess('渠道更新成功!');
showSuccess(t('渠道更新成功!'));
} else {
showSuccess('渠道创建成功!');
showSuccess(t('渠道创建成功!'));
setInputs(originInputs);
}
props.refresh();
@@ -363,7 +365,6 @@ const EditChannel = (props) => {
const addCustomModels = () => {
if (customModel.trim() === '') return;
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
const modelArray = customModel.split(',').map((model) => model.trim());
let localModels = [...inputs.models];
@@ -371,24 +372,21 @@ const EditChannel = (props) => {
let hasError = false;
modelArray.forEach((model) => {
// 检查模型是否已存在,且模型名称非空
if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表
localModels.push(model);
localModelOptions.push({
// 添加到下拉选项
key: model,
text: model,
value: model
});
} else if (model) {
showError('某些模型已存在!');
showError(t('某些模型已存在!'));
hasError = true;
}
});
if (hasError) return; // 如果有错误则终止操作
if (hasError) return;
// 更新状态值
setModelOptions(localModelOptions);
setCustomModel('');
handleInputChange('models', localModels);
@@ -401,7 +399,7 @@ const EditChannel = (props) => {
maskClosable={false}
placement={isEdit ? 'right' : 'left'}
title={
<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>
<Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -410,7 +408,7 @@ const EditChannel = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme="solid" size={'large'} onClick={submit}>
提交
{t('提交')}
</Button>
<Button
theme="solid"
@@ -418,7 +416,7 @@ const EditChannel = (props) => {
type={'tertiary'}
onClick={handleCancel}
>
取消
{t('取消')}
</Button>
</Space>
</div>
@@ -429,7 +427,7 @@ const EditChannel = (props) => {
>
<Spin spinning={loading}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>类型</Typography.Text>
<Typography.Text strong>{t('类型')}</Typography.Text>
</div>
<Select
name="type"
@@ -444,20 +442,7 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}>
<Banner
type={'warning'}
description={
<>
注意<strong>模型部署名称必须和模型名称保持一致</strong>
因为 One API 会把请求体中的 model
参数替换为你的部署名称模型名称中的点会被剔除
<a
target="_blank"
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
>
图片演示
</a>
</>
}
description={t('注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model 参数替换为你的部署名称(模型名称中的点会被剔除)')}
></Banner>
</div>
<div style={{ marginTop: 10 }}>
@@ -468,9 +453,7 @@ const EditChannel = (props) => {
<Input
label="AZURE_OPENAI_ENDPOINT"
name="azure_base_url"
placeholder={
'请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com'
}
placeholder={t('请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com')}
onChange={(value) => {
handleInputChange('base_url', value);
}}
@@ -478,14 +461,12 @@ const EditChannel = (props) => {
autoComplete="new-password"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>默认 API 版本</Typography.Text>
<Typography.Text strong>{t('默认 API 版本')}</Typography.Text>
</div>
<Input
label="默认 API 版本"
label={t('默认 API 版本')}
name="azure_other"
placeholder={
'请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖'
}
placeholder={t('请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖')}
onChange={(value) => {
handleInputChange('other', value);
}}
@@ -499,23 +480,17 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}>
<Banner
type={'warning'}
description={
<>
如果你对接的是上游One API或者New API等转发项目请使用OpenAI类型不要使用此类型除非你知道你在做什么
</>
}
description={t('如果你对接的是上游One API或者New API等转发项目请使用OpenAI类型不要使用此类型除非你知道你在做什么。')}
></Banner>
</div>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
完整的 Base URL支持变量{'{model}'}
{t('完整的 Base URL支持变量{model}')}
</Typography.Text>
</div>
<Input
name="base_url"
placeholder={
'请输入完整的URL例如https://api.openai.com/v1/chat/completions'
}
placeholder={t('请输入完整的URL例如https://api.openai.com/v1/chat/completions')}
onChange={(value) => {
handleInputChange('base_url', value);
}}
@@ -527,12 +502,12 @@ const EditChannel = (props) => {
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>代理</Typography.Text>
<Typography.Text strong>{t('代理')}</Typography.Text>
</div>
<Input
label="代理"
label={t('代理')}
name="base_url"
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
placeholder={t('此项可选,用于通过代理站来进行 API 调用')}
onChange={(value) => {
handleInputChange('base_url', value);
}}
@@ -544,13 +519,11 @@ const EditChannel = (props) => {
{inputs.type === 22 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>私有部署地址</Typography.Text>
<Typography.Text strong>{t('私有部署地址')}</Typography.Text>
</div>
<Input
name="base_url"
placeholder={
'请输入私有部署地址格式为https://fastgpt.run/api/openapi'
}
placeholder={t('请输入私有部署地址格式为https://fastgpt.run/api/openapi')}
onChange={(value) => {
handleInputChange('base_url', value);
}}
@@ -563,14 +536,12 @@ const EditChannel = (props) => {
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
注意非Chat API请务必填写正确的API地址否则可能导致无法使用
{t('注意非Chat API请务必填写正确的API地址否则可能导致无法使用')}
</Typography.Text>
</div>
<Input
name="base_url"
placeholder={
'请输入到 /suno 前的路径通常就是域名例如https://api.example.com '
}
placeholder={t('请输入到 /suno 前的路径通常就是域名例如https://api.example.com')}
onChange={(value) => {
handleInputChange('base_url', value);
}}
@@ -580,12 +551,12 @@ const EditChannel = (props) => {
</>
)}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>名称</Typography.Text>
<Typography.Text strong>{t('名称')}</Typography.Text>
</div>
<Input
required
name="name"
placeholder={'请为渠道命名'}
placeholder={t('请为渠道命名')}
onChange={(value) => {
handleInputChange('name', value);
}}
@@ -593,16 +564,16 @@ const EditChannel = (props) => {
autoComplete="new-password"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text>
<Typography.Text strong>{t('分组')}</Typography.Text>
</div>
<Select
placeholder={'请选择可以使用该渠道的分组'}
placeholder={t('请选择可以使用该渠道的分组')}
name="groups"
required
multiple
selection
allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
onChange={(value) => {
handleInputChange('groups', value);
}}
@@ -631,17 +602,15 @@ const EditChannel = (props) => {
{inputs.type === 41 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>部署地区</Typography.Text>
<Typography.Text strong>{t('部署地区')}</Typography.Text>
</div>
<TextArea
name="other"
placeholder={
'请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
placeholder={t('请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
'{\n' +
' "default": "us-central1",\n' +
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
'}'
}
'}')}
autosize={{ minRows: 2 }}
onChange={(value) => {
handleInputChange('other', value);
@@ -662,7 +631,7 @@ const EditChannel = (props) => {
);
}}
>
填入模板
{t('填入模板')}
</Typography.Text>
</>
)}
@@ -702,7 +671,7 @@ const EditChannel = (props) => {
</>
)}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型</Typography.Text>
<Typography.Text strong>{t('模型')}</Typography.Text>
</div>
<Select
placeholder={'请选择该渠道所支持的模型'}
@@ -727,7 +696,7 @@ const EditChannel = (props) => {
handleInputChange('models', basicModels);
}}
>
填入相关模型
{t('填入相关模型')}
</Button>
<Button
type="secondary"
@@ -735,16 +704,16 @@ const EditChannel = (props) => {
handleInputChange('models', fullModels);
}}
>
填入所有模型
{t('填入所有模型')}
</Button>
<Tooltip content={fetchButtonTips}>
<Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
<Button
type="tertiary"
onClick={() => {
fetchUpstreamModelList('models');
}}
>
获取模型列表
{t('获取模型列表')}
</Button>
</Tooltip>
<Button
@@ -753,16 +722,16 @@ const EditChannel = (props) => {
handleInputChange('models', []);
}}
>
清除所有模型
{t('清除所有模型')}
</Button>
</Space>
<Input
addonAfter={
<Button type="primary" onClick={addCustomModels}>
填入
{t('填入')}
</Button>
}
placeholder="输入自定义模型名称"
placeholder={t('输入自定义模型名称')}
value={customModel}
onChange={(value) => {
setCustomModel(value.trim());
@@ -770,10 +739,10 @@ const EditChannel = (props) => {
/>
</div>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型重定向</Typography.Text>
<Typography.Text strong>{t('模型重定向')}</Typography.Text>
</div>
<TextArea
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
name="model_mapping"
onChange={(value) => {
handleInputChange('model_mapping', value);
@@ -795,17 +764,17 @@ const EditChannel = (props) => {
);
}}
>
填入模板
{t('填入模板')}
</Typography.Text>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>密钥</Typography.Text>
<Typography.Text strong>{t('密钥')}</Typography.Text>
</div>
{batch ? (
<TextArea
label="密钥"
label={t('密钥')}
name="key"
required
placeholder={'请输入密钥,一行一个'}
placeholder={t('请输入密钥,一行一个')}
onChange={(value) => {
handleInputChange('key', value);
}}
@@ -817,7 +786,7 @@ const EditChannel = (props) => {
<>
{inputs.type === 41 ? (
<TextArea
label="鉴权json"
label={t('鉴权json')}
name="key"
required
placeholder={'{\n' +
@@ -842,18 +811,17 @@ const EditChannel = (props) => {
/>
) : (
<Input
label="密钥"
label={t('密钥')}
name="key"
required
placeholder={type2secretPrompt(inputs.type)}
placeholder={t(type2secretPrompt(inputs.type))}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
autoComplete="new-password"
/>
)
}
)}
</>
)}
{!isEdit && (
@@ -861,23 +829,23 @@ const EditChannel = (props) => {
<Space>
<Checkbox
checked={batch}
label="批量创建"
label={t('批量创建')}
name="batch"
onChange={() => setBatch(!batch)}
/>
<Typography.Text strong>批量创建</Typography.Text>
<Typography.Text strong>{t('批量创建')}</Typography.Text>
</Space>
</div>
)}
{inputs.type === 1 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>组织</Typography.Text>
<Typography.Text strong>{t('组织')}</Typography.Text>
</div>
<Input
label="组织,可选,不填则为默认组织"
label={t('组织,可选,不填则为默认组织')}
name="openai_organization"
placeholder="请输入组织org-xxx"
placeholder={t('请输入组织org-xxx')}
onChange={(value) => {
handleInputChange('openai_organization', value);
}}
@@ -886,11 +854,11 @@ const EditChannel = (props) => {
</>
)}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>默认测试模型</Typography.Text>
<Typography.Text strong>{t('默认测试模型')}</Typography.Text>
</div>
<Input
name="test_model"
placeholder="不填则为模型列表第一个"
placeholder={t('不填则为模型列表第一个')}
onChange={(value) => {
handleInputChange('test_model', value);
}}
@@ -904,20 +872,20 @@ const EditChannel = (props) => {
onChange={() => {
setAutoBan(!autoBan);
}}
// onChange={handleInputChange}
/>
<Typography.Text strong>
是否自动禁用仅当自动禁用开启时有效关闭后不会自动禁用该渠道
{t('是否自动禁用仅当自动禁用开启时有效关闭后不会自动禁用该渠道:')}
</Typography.Text>
</Space>
</div>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
状态码复写仅影响本地判断不修改返回到上游的状态码
{t('状态码复写(仅影响本地判断不修改返回到上游的状态码)')}
</Typography.Text>
</div>
<TextArea
placeholder={`此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
placeholder={t('此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如') +
'\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
name="status_code_mapping"
onChange={(value) => {
handleInputChange('status_code_mapping', value);
@@ -939,17 +907,17 @@ const EditChannel = (props) => {
);
}}
>
填入模板
{t('填入模板')}
</Typography.Text>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
渠道标签
{t('渠道标签')}
</Typography.Text>
</div>
<Input
label="渠道标签"
label={t('渠道标签')}
name="tag"
placeholder={'渠道标签'}
placeholder={t('渠道标签')}
onChange={(value) => {
handleInputChange('tag', value);
}}
@@ -958,13 +926,13 @@ const EditChannel = (props) => {
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
渠道优先级
{t('渠道优先级')}
</Typography.Text>
</div>
<Input
label="渠道优先级"
label={t('渠道优先级')}
name="priority"
placeholder={'渠道优先级'}
placeholder={t('渠道优先级')}
onChange={(value) => {
const number = parseInt(value);
if (isNaN(number)) {
@@ -978,13 +946,13 @@ const EditChannel = (props) => {
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
渠道权重
{t('渠道权重')}
</Typography.Text>
</div>
<Input
label="渠道权重"
label={t('渠道权重')}
name="weight"
placeholder={'渠道权重'}
placeholder={t('渠道权重')}
onChange={(value) => {
const number = parseInt(value);
if (isNaN(number)) {

View File

@@ -1,18 +1,22 @@
import React from 'react';
import ChannelsTable from '../../components/ChannelsTable';
import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const File = () => (
<>
<Layout>
<Layout.Header>
<h3>管理渠道</h3>
const File = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<h3>{t('管理渠道')}</h3>
</Layout.Header>
<Layout.Content>
<ChannelsTable />
</Layout.Content>
</Layout>
</>
);
</Layout.Content>
</Layout>
</>
);
};
export default File;

View File

@@ -21,8 +21,10 @@ import {
} from '../../helpers/render';
import { UserContext } from '../../context/User/index.js';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const Detail = (props) => {
const { t } = useTranslation();
const formRef = useRef();
let now = new Date();
const [userState, userDispatch] = useContext(UserContext);
@@ -85,8 +87,8 @@ const Detail = (props) => {
},
title: {
visible: true,
text: '模型调用次数占比',
subtext: `总计${renderNumber(times)}`,
text: t('模型调用次数占比'),
subtext: `${t('总计')}${renderNumber(times)}`,
},
legends: {
visible: true,
@@ -125,11 +127,10 @@ const Detail = (props) => {
},
title: {
visible: true,
text: '模型消耗分布',
subtext: `总计${renderQuota(consumeQuota, 2)}`,
text: t('模型消耗分布'),
subtext: `${t('总计')}${renderQuota(consumeQuota, 2)}`,
},
bar: {
// The state style of bar
state: {
hover: {
stroke: '#000',
@@ -155,9 +156,7 @@ const Detail = (props) => {
},
],
updateContent: (array) => {
// sort by value
array.sort((a, b) => b.value - a.value);
// add $
let sum = 0;
for (let i = 0; i < array.length; i++) {
sum += parseFloat(array[i].value);
@@ -166,9 +165,8 @@ const Detail = (props) => {
4,
);
}
// add to first
array.unshift({
key: '总计',
key: t('总计'),
value: renderQuotaNumberWithDigit(sum, 4),
});
return array;
@@ -331,7 +329,7 @@ const Detail = (props) => {
data: [{ id: 'id0', values: newPieData }],
title: {
...prev.title,
subtext: `总计${renderNumber(totalTimes)}`
subtext: `${t('总计')}${renderNumber(totalTimes)}`
},
color: {
specified: newModelColors
@@ -343,7 +341,7 @@ const Detail = (props) => {
data: [{ id: 'barData', values: newLineData }],
title: {
...prev.title,
subtext: `总计${renderQuota(totalQuota, 2)}`
subtext: `${t('总计')}${renderQuota(totalQuota, 2)}`
},
color: {
specified: newModelColors
@@ -382,14 +380,14 @@ const Detail = (props) => {
<>
<Layout>
<Layout.Header>
<h3>数据看板</h3>
<h3>{t('数据看板')}</h3>
</Layout.Header>
<Layout.Content>
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
<>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
@@ -402,7 +400,7 @@ const Detail = (props) => {
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
@@ -412,15 +410,15 @@ const Detail = (props) => {
/>
<Form.Select
field='data_export_default_time'
label='时间粒度'
label={t('时间粒度')}
style={{ width: 176 }}
initValue={dataExportDefaultTime}
placeholder={'时间粒度'}
placeholder={t('时间粒度')}
name='data_export_default_time'
optionList={[
{ label: '小时', value: 'hour' },
{ label: '天', value: 'day' },
{ label: '周', value: 'week' },
{ label: t('小时'), value: 'hour' },
{ label: t('天'), value: 'day' },
{ label: t('周'), value: 'week' },
]}
onChange={(value) =>
handleInputChange(value, 'data_export_default_time')
@@ -430,17 +428,17 @@ const Detail = (props) => {
<>
<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'
@@ -448,7 +446,7 @@ const Detail = (props) => {
loading={loading}
style={{ marginTop: 24 }}
>
查询
{t('查询')}
</Button>
<Form.Section>
</Form.Section>
@@ -459,13 +457,13 @@ const Detail = (props) => {
<Col span={styleState.isMobile?24:8}>
<Card className='panel-desc-card'>
<Descriptions row size="small">
<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>
@@ -474,13 +472,13 @@ const Detail = (props) => {
<Col span={styleState.isMobile?24:8}>
<Card>
<Descriptions row size="small">
<Descriptions.Item itemKey='统计额度'>
<Descriptions.Item itemKey={t('统计额度')}>
{renderQuota(consumeQuota)}
</Descriptions.Item>
<Descriptions.Item itemKey='统计Tokens'>
<Descriptions.Item itemKey={t('统计Tokens')}>
{consumeTokens}
</Descriptions.Item>
<Descriptions.Item itemKey='统计次数'>
<Descriptions.Item itemKey={t('统计次数')}>
{times}
</Descriptions.Item>
</Descriptions>
@@ -489,13 +487,13 @@ const Detail = (props) => {
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey='平均RPM'>
<Descriptions.Item itemKey={t('平均RPM')}>
{(times /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
<Descriptions.Item itemKey='平均TPM'>
<Descriptions.Item itemKey={t('平均TPM')}>
{(consumeTokens /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
@@ -507,7 +505,7 @@ const Detail = (props) => {
</Row>
<Card style={{marginTop: 20}}>
<Tabs type="line" defaultActiveKey="1">
<Tabs.TabPane tab="消耗分布" itemKey="1">
<Tabs.TabPane tab={t('消耗分布')} itemKey="1">
<div style={{ height: 500 }}>
<VChart
spec={spec_line}
@@ -515,7 +513,7 @@ const Detail = (props) => {
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="调用次数分布" itemKey="2">
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
<div style={{ height: 500 }}>
<VChart
spec={spec_pie}

View File

@@ -6,24 +6,10 @@ import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button
import { SSE } from 'sse';
import { IconSetting } from '@douyinfe/semi-icons';
import { StyleContext } from '../../context/Style/index.js';
const defaultMessage = [
{
role: 'user',
id: '2',
createAt: 1715676751919,
content: "你好",
},
{
role: 'assistant',
id: '3',
createAt: 1715676751919,
content: "你好,请问有什么可以帮助您的吗?",
}
];
import { useTranslation } from 'react-i18next';
const roleInfo = {
user: {
user: {
name: 'User',
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
},
@@ -43,6 +29,23 @@ function getId() {
}
const Playground = () => {
const { t } = useTranslation();
const defaultMessage = [
{
role: 'user',
id: '2',
createAt: 1715676751919,
content: t('你好'),
},
{
role: 'assistant',
id: '3',
createAt: 1715676751919,
content: t('你好,请问有什么可以帮助您的吗?'),
}
];
const [inputs, setInputs] = useState({
model: 'gpt-4o-mini',
group: '',
@@ -65,7 +68,7 @@ const Playground = () => {
useEffect(() => {
if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录!');
showError(t('未登录或登录已过期,请重新登录!'));
}
let status = localStorage.getItem('status');
if (status) {
@@ -86,7 +89,7 @@ const Playground = () => {
}));
setModels(localModelOptions);
} else {
showError(message);
showError(t(message));
}
};
@@ -115,7 +118,7 @@ const Playground = () => {
}
} else {
localGroupOptions = [{
label: '用户分组',
label: t('用户分组'),
value: '',
}];
setGroups(localGroupOptions);
@@ -123,7 +126,7 @@ const Playground = () => {
setGroups(localGroupOptions);
handleInputChange('group', localGroupOptions[0].value);
} else {
showError(message);
showError(t(message));
}
};
@@ -314,10 +317,10 @@ const Playground = () => {
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
<Card style={commonOuterStyle}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text>
<Typography.Text strong>{t('分组')}</Typography.Text>
</div>
<Select
placeholder={'请选择分组'}
placeholder={t('请选择分组')}
name='group'
required
selection
@@ -334,10 +337,10 @@ const Playground = () => {
}))}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型</Typography.Text>
<Typography.Text strong>{t('模型')}</Typography.Text>
</div>
<Select
placeholder={'请选择模型'}
placeholder={t('请选择模型')}
name='model'
required
selection

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
API,
downloadTextAsFile,
@@ -22,6 +23,7 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
const EditRedemption = (props) => {
const { t } = useTranslation();
const isEdit = props.editingRedemption.id !== undefined;
const [loading, setLoading] = useState(isEdit);
@@ -69,7 +71,7 @@ const EditRedemption = (props) => {
let name = inputs.name;
if (!isEdit && inputs.name === '') {
// set default name
name = '兑换码-' + renderQuota(quota);
name = t('新建兑换码') + ' ' + renderQuota(quota);
}
setLoading(true);
let localInputs = inputs;
@@ -90,11 +92,11 @@ const EditRedemption = (props) => {
const { success, message, data } = res.data;
if (success) {
if (isEdit) {
showSuccess('兑换码更新成功!');
showSuccess(t('兑换码更新成功!'));
props.refresh();
props.handleClose();
} else {
showSuccess('兑换码创建成功!');
showSuccess(t('兑换码创建成功!'));
setInputs(originInputs);
props.refresh();
props.handleClose();
@@ -107,13 +109,12 @@ const EditRedemption = (props) => {
for (let i = 0; i < data.length; i++) {
text += data[i] + '\n';
}
// downloadTextAsFile(text, `${inputs.name}.txt`);
Modal.confirm({
title: '兑换码创建成功',
title: t('兑换码创建成功'),
content: (
<div>
<p>兑换码创建成功是否下载兑换码</p>
<p>兑换码将以文本文件的形式下载文件名为兑换码的名称</p>
<p>{t('兑换码创建成功是否下载兑换码?')}</p>
<p>{t('兑换码将以文本文件的形式下载文件名为兑换码的名称。')}</p>
</div>
),
onOk: () => {
@@ -130,7 +131,7 @@ const EditRedemption = (props) => {
placement={isEdit ? 'right' : 'left'}
title={
<Title level={3}>
{isEdit ? '更新兑换码信息' : '创建新的兑换码'}
{isEdit ? t('更新兑换码信息') : t('创建新的兑换码')}
</Title>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -140,7 +141,7 @@ const EditRedemption = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme='solid' size={'large'} onClick={submit}>
提交
{t('提交')}
</Button>
<Button
theme='solid'
@@ -148,7 +149,7 @@ const EditRedemption = (props) => {
type={'tertiary'}
onClick={handleCancel}
>
取消
{t('取消')}
</Button>
</Space>
</div>
@@ -160,9 +161,9 @@ const EditRedemption = (props) => {
<Spin spinning={loading}>
<Input
style={{ marginTop: 20 }}
label='名称'
label={t('名称')}
name='name'
placeholder={'请输入名称'}
placeholder={t('请输入名称')}
onChange={(value) => handleInputChange('name', value)}
value={name}
autoComplete='new-password'
@@ -170,12 +171,12 @@ const EditRedemption = (props) => {
/>
<Divider />
<div style={{ marginTop: 20 }}>
<Typography.Text>{`额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
<Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text>
</div>
<AutoComplete
style={{ marginTop: 8 }}
name='quota'
placeholder={'请输入额度'}
placeholder={t('请输入额度')}
onChange={(value) => handleInputChange('quota', value)}
value={quota}
autoComplete='new-password'
@@ -193,12 +194,12 @@ const EditRedemption = (props) => {
{!isEdit && (
<>
<Divider />
<Typography.Text>生成数量</Typography.Text>
<Typography.Text>{t('生成数量')}</Typography.Text>
<Input
style={{ marginTop: 8 }}
label='生成数量'
label={t('生成数量')}
name='count'
placeholder={'请输入生成数量'}
placeholder={t('请输入生成数量')}
onChange={(value) => handleInputChange('count', value)}
value={count}
autoComplete='new-password'

View File

@@ -1,12 +1,15 @@
import React from 'react';
import RedemptionsTable from '../../components/RedemptionsTable';
import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Redemption = () => (
<>
<Layout>
<Layout.Header>
<h3>管理兑换码</h3>
const Redemption = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<h3>{t('管理兑换码')}</h3>
</Layout.Header>
<Layout.Content>
<RedemptionsTable />
@@ -14,5 +17,6 @@ const Redemption = () => (
</Layout>
</>
);
}
export default Redemption;

View File

@@ -9,8 +9,10 @@ import {
verifyJSON,
verifyJSONPromise
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsChats(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
Chats: "[]",
@@ -24,7 +26,7 @@ export default function SettingsChats(props) {
await refForm.current.validate().then(() => {
console.log('Validation passed');
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -44,23 +46,23 @@ export default function SettingsChats(props) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError('部分保存失败,请重试');
return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}).catch((error) => {
console.error('Validation failed:', error);
showError('请检查输入');
showError(t('请检查输入'));
});
} catch (error) {
showError('请检查输入');
showError(t('请检查输入'));
console.error(error);
}
}
@@ -104,19 +106,19 @@ export default function SettingsChats(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'令牌聊天设置'}>
<Form.Section text={t('令牌聊天设置')}>
<Banner
type='warning'
description={'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能'}
description={t('必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能')}
/>
<Banner
type='info'
description={'链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1'}
description={t('链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1')}
/>
<Form.TextArea
label={'聊天配置'}
label={t('聊天配置')}
extraText={''}
placeholder={'为一个 JSON 文本'}
placeholder={t('为一个 JSON 文本')}
field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
@@ -126,7 +128,7 @@ export default function SettingsChats(props) {
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
message: t('不是合法的 JSON 字符串')
}
]}
onChange={(value) =>
@@ -140,7 +142,7 @@ export default function SettingsChats(props) {
</Form>
<Space>
<Button onClick={onSubmit}>
保存聊天设置
{t('保存聊天设置')}
</Button>
</Space>
</Spin>

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
import {
compareObjects,
API,
@@ -9,6 +10,7 @@ import {
} from '../../../helpers';
export default function SettingsCreditLimit(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
QuotaForNewUser: '',
@@ -21,7 +23,7 @@ export default function SettingsCreditLimit(props) {
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -40,13 +42,13 @@ export default function SettingsCreditLimit(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -72,11 +74,11 @@ export default function SettingsCreditLimit(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'额度设置'}>
<Form.Section text={t('额度设置')}>
<Row gutter={16}>
<Col span={6}>
<Form.InputNumber
label={'新用户初始额度'}
label={t('新用户初始额度')}
field={'QuotaForNewUser'}
step={1}
min={0}
@@ -92,12 +94,12 @@ export default function SettingsCreditLimit(props) {
</Col>
<Col span={6}>
<Form.InputNumber
label={'请求预扣费额度'}
label={t('请求预扣费额度')}
field={'PreConsumedQuota'}
step={1}
min={0}
suffix={'Token'}
extraText={'请求结束后多退少补'}
extraText={t('请求结束后多退少补')}
placeholder={''}
onChange={(value) =>
setInputs({
@@ -109,13 +111,13 @@ export default function SettingsCreditLimit(props) {
</Col>
<Col span={6}>
<Form.InputNumber
label={'邀请新用户奖励额度'}
label={t('邀请新用户奖励额度')}
field={'QuotaForInviter'}
step={1}
min={0}
suffix={'Token'}
extraText={''}
placeholder={'例如2000'}
placeholder={t('例如2000')}
onChange={(value) =>
setInputs({
...inputs,
@@ -126,13 +128,13 @@ export default function SettingsCreditLimit(props) {
</Col>
<Col span={6}>
<Form.InputNumber
label={'新用户使用邀请码奖励额度'}
label={t('新用户使用邀请码奖励额度')}
field={'QuotaForInvitee'}
step={1}
min={0}
suffix={'Token'}
extraText={''}
placeholder={'例如1000'}
placeholder={t('例如1000')}
onChange={(value) =>
setInputs({
...inputs,
@@ -145,7 +147,7 @@ export default function SettingsCreditLimit(props) {
<Row>
<Button size='default' onClick={onSubmit}>
保存额度设置
{t('保存额度设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
@@ -7,12 +7,15 @@ import {
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function DataDashboard(props) {
const { t } = useTranslation();
const optionsDataExportDefaultTime = [
{ key: 'hour', label: '小时', value: 'hour' },
{ key: 'day', label: '天', value: 'day' },
{ key: 'week', label: '周', value: 'week' },
{ key: 'hour', label: t('小时'), value: 'hour' },
{ key: 'day', label: t('天'), value: 'day' },
{ key: 'week', label: t('周'), value: 'week' },
];
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
@@ -25,7 +28,7 @@ export default function DataDashboard(props) {
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -44,13 +47,13 @@ export default function DataDashboard(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -81,12 +84,12 @@ export default function DataDashboard(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'数据看板设置'}>
<Form.Section text={t('数据看板设置')}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'DataExportEnabled'}
label={'启用数据看板(实验性)'}
label={t('启用数据看板(实验性)')}
size='default'
checkedText=''
uncheckedText=''
@@ -102,12 +105,12 @@ export default function DataDashboard(props) {
<Row>
<Col span={8}>
<Form.InputNumber
label={'数据看板更新间隔 '}
label={t('数据看板更新间隔')}
step={1}
min={1}
suffix={'分钟'}
extraText={'设置过短会影响数据库性能'}
placeholder={'数据看板更新间隔'}
suffix={t('分钟')}
extraText={t('设置过短会影响数据库性能')}
placeholder={t('数据看板更新间隔')}
field={'DataExportInterval'}
onChange={(value) =>
setInputs({
@@ -119,11 +122,11 @@ export default function DataDashboard(props) {
</Col>
<Col span={8}>
<Form.Select
label='数据看板默认时间粒度'
label={t('数据看板默认时间粒度')}
optionList={optionsDataExportDefaultTime}
field={'DataExportDefaultTime'}
extraText={'仅修改展示粒度,统计精确到小时'}
placeholder={'数据看板默认时间粒度'}
extraText={t('仅修改展示粒度,统计精确到小时')}
placeholder={t('数据看板默认时间粒度')}
style={{ width: 180 }}
onChange={(value) =>
setInputs({
@@ -136,7 +139,7 @@ export default function DataDashboard(props) {
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
保存数据看板设置
{t('保存数据看板设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsDrawing(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
DrawingEnabled: false,
@@ -23,7 +25,7 @@ export default function SettingsDrawing(props) {
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -42,13 +44,13 @@ export default function SettingsDrawing(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -67,6 +69,7 @@ export default function SettingsDrawing(props) {
refForm.current.setValues(currentInputs);
localStorage.setItem('mj_notify_enabled', String(inputs.MjNotifyEnabled));
}, [props.options]);
return (
<>
<Spin spinning={loading}>
@@ -75,12 +78,12 @@ export default function SettingsDrawing(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'绘图设置'}>
<Form.Section text={t('绘图设置')}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'DrawingEnabled'}
label={'启用绘图功能'}
label={t('启用绘图功能')}
size='default'
checkedText=''
uncheckedText=''
@@ -95,7 +98,7 @@ export default function SettingsDrawing(props) {
<Col span={8}>
<Form.Switch
field={'MjNotifyEnabled'}
label={'允许回调(会泄露服务器 IP 地址)'}
label={t('允许回调(会泄露服务器 IP 地址)')}
size='default'
checkedText=''
uncheckedText=''
@@ -110,7 +113,7 @@ export default function SettingsDrawing(props) {
<Col span={8}>
<Form.Switch
field={'MjAccountFilterEnabled'}
label={'允许 AccountFilter 参数'}
label={t('允许 AccountFilter 参数')}
size='default'
checkedText=''
uncheckedText=''
@@ -125,7 +128,7 @@ export default function SettingsDrawing(props) {
<Col span={8}>
<Form.Switch
field={'MjForwardUrlEnabled'}
label={'开启之后将上游地址替换为服务器地址'}
label={t('开启之后将上游地址替换为服务器地址')}
size='default'
checkedText=''
uncheckedText=''
@@ -142,8 +145,8 @@ export default function SettingsDrawing(props) {
field={'MjModeClearEnabled'}
label={
<>
开启之后会清除用户提示词中的 <Tag>--fast</Tag>
<Tag>--relax</Tag> <Tag>--turbo</Tag>
{t('开启之后会清除用户提示词中的')} <Tag>--fast</Tag>
<Tag>--relax</Tag> {t('')} <Tag>--turbo</Tag> {t('')}
</>
}
size='default'
@@ -160,11 +163,7 @@ export default function SettingsDrawing(props) {
<Col span={8}>
<Form.Switch
field={'MjActionCheckSuccessEnabled'}
label={
<>
检测必须等待绘图成功才能进行放大等操作
</>
}
label={t('检测必须等待绘图成功才能进行放大等操作')}
size='default'
checkedText=''
uncheckedText=''
@@ -179,7 +178,7 @@ export default function SettingsDrawing(props) {
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
保存绘图设置
{t('保存绘图设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function GeneralSettings(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
TopUpLink: '',
@@ -22,13 +24,15 @@ export default function GeneralSettings(props) {
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onChange(value, e) {
const name = e.target.id;
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -47,13 +51,13 @@ export default function GeneralSettings(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -71,26 +75,27 @@ export default function GeneralSettings(props) {
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Banner
type='warning'
description={'聊天链接功能已经弃用,请使用下方聊天设置功能'}
description={t('聊天链接功能已经弃用,请使用下方聊天设置功能')}
/>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'通用设置'}>
<Form.Section text={t('通用设置')}>
<Row gutter={16}>
<Col span={8}>
<Form.Input
field={'TopUpLink'}
label={'充值链接'}
label={t('充值链接')}
initValue={''}
placeholder={'例如发卡网站的购买链接'}
placeholder={t('例如发卡网站的购买链接')}
onChange={onChange}
showClear
/>
@@ -98,9 +103,9 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Input
field={'ChatLink'}
label={'默认聊天页面链接'}
label={t('默认聊天页面链接')}
initValue={''}
placeholder='例如 ChatGPT Next Web 的部署地址'
placeholder={t('例如 ChatGPT Next Web 的部署地址')}
onChange={onChange}
showClear
/>
@@ -108,9 +113,9 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Input
field={'ChatLink2'}
label={'聊天页面 2 链接'}
label={t('聊天页面 2 链接')}
initValue={''}
placeholder='例如 ChatGPT Next Web 的部署地址'
placeholder={t('例如 ChatGPT Next Web 的部署地址')}
onChange={onChange}
showClear
/>
@@ -118,9 +123,9 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Input
field={'QuotaPerUnit'}
label={'单位美元额度'}
label={t('单位美元额度')}
initValue={''}
placeholder='一单位货币能兑换的额度'
placeholder={t('一单位货币能兑换的额度')}
onChange={onChange}
showClear
/>
@@ -128,9 +133,9 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Input
field={'RetryTimes'}
label={'失败重试次数'}
label={t('失败重试次数')}
initValue={''}
placeholder='失败重试次数'
placeholder={t('失败重试次数')}
onChange={onChange}
showClear
/>
@@ -140,7 +145,7 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Switch
field={'DisplayInCurrencyEnabled'}
label={'以货币形式显示额度'}
label={t('以货币形式显示额度')}
size='default'
checkedText=''
uncheckedText=''
@@ -155,7 +160,7 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Switch
field={'DisplayTokenStatEnabled'}
label={'Billing 相关 API 显示令牌额度而非用户额度'}
label={t('额度查询接口返回令牌额度而非用户额度')}
size='default'
checkedText=''
uncheckedText=''
@@ -170,7 +175,7 @@ export default function GeneralSettings(props) {
<Col span={8}>
<Form.Switch
field={'DefaultCollapseSidebar'}
label={'默认折叠侧边栏'}
label={t('默认折叠侧边栏')}
size='default'
checkedText=''
uncheckedText=''
@@ -185,7 +190,7 @@ export default function GeneralSettings(props) {
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
保存通用设置
{t('保存通用设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import {
compareObjects,
API,
@@ -10,6 +11,7 @@ import {
} from '../../../helpers';
export default function SettingsLog(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [loadingCleanHistoryLog, setLoadingCleanHistoryLog] = useState(false);
const [inputs, setInputs] = useState({
@@ -24,7 +26,7 @@ export default function SettingsLog(props) {
(item) => item.key !== 'historyTimestamp',
);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -43,13 +45,13 @@ export default function SettingsLog(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -58,16 +60,16 @@ export default function SettingsLog(props) {
async function onCleanHistoryLog() {
try {
setLoadingCleanHistoryLog(true);
if (!inputs.historyTimestamp) throw new Error('请选择日志记录时间');
if (!inputs.historyTimestamp) throw new Error(t('请选择日志记录时间'));
const res = await API.delete(
`/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
);
const { success, message, data } = res.data;
if (success) {
showSuccess(`${data} 条日志已清理!`);
showSuccess(`${data} ${t('条日志已清理!')}`);
return;
} else {
throw new Error('日志清理失败:' + message);
throw new Error(t('日志清理失败:') + message);
}
} catch (error) {
showError(error.message);
@@ -96,12 +98,12 @@ export default function SettingsLog(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'日志设置'}>
<Form.Section text={t('日志设置')}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'LogConsumeEnabled'}
label={'启用额度消费日志记录'}
label={t('启用额度消费日志记录')}
size='default'
checkedText=''
uncheckedText=''
@@ -116,7 +118,7 @@ export default function SettingsLog(props) {
<Col span={8}>
<Spin spinning={loadingCleanHistoryLog}>
<Form.DatePicker
label='日志记录时间'
label={t('日志记录时间')}
field={'historyTimestamp'}
type='dateTime'
inputReadOnly={true}
@@ -128,7 +130,7 @@ export default function SettingsLog(props) {
}}
/>
<Button size='default' onClick={onCleanHistoryLog}>
清除历史日志
{t('清除历史日志')}
</Button>
</Spin>
</Col>
@@ -136,7 +138,7 @@ export default function SettingsLog(props) {
<Row>
<Button size='default' onClick={onSubmit}>
保存日志设置
{t('保存日志设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsMonitoring(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ChannelDisableThreshold: '',
@@ -21,7 +23,7 @@ export default function SettingsMonitoring(props) {
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -40,13 +42,13 @@ export default function SettingsMonitoring(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -64,6 +66,7 @@ export default function SettingsMonitoring(props) {
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
@@ -72,15 +75,15 @@ export default function SettingsMonitoring(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'监控设置'}>
<Form.Section text={t('监控设置')}>
<Row gutter={16}>
<Col span={8}>
<Form.InputNumber
label={'最长响应时间'}
label={t('最长响应时间')}
step={1}
min={0}
suffix={'秒'}
extraText={'当运行通道全部测试时,超过此时间将自动禁用通道'}
suffix={t('秒')}
extraText={t('当运行通道全部测试时,超过此时间将自动禁用通道')}
placeholder={''}
field={'ChannelDisableThreshold'}
onChange={(value) =>
@@ -93,11 +96,11 @@ export default function SettingsMonitoring(props) {
</Col>
<Col span={8}>
<Form.InputNumber
label={'额度提醒阈值'}
label={t('额度提醒阈值')}
step={1}
min={0}
suffix={'Token'}
extraText={'低于此额度时将发送邮件提醒用户'}
extraText={t('低于此额度时将发送邮件提醒用户')}
placeholder={''}
field={'QuotaRemindThreshold'}
onChange={(value) =>
@@ -113,7 +116,7 @@ export default function SettingsMonitoring(props) {
<Col span={8}>
<Form.Switch
field={'AutomaticDisableChannelEnabled'}
label={'失败时自动禁用通道'}
label={t('失败时自动禁用通道')}
size='default'
checkedText=''
uncheckedText=''
@@ -128,7 +131,7 @@ export default function SettingsMonitoring(props) {
<Col span={8}>
<Form.Switch
field={'AutomaticEnableChannelEnabled'}
label={'成功时自动启用通道'}
label={t('成功时自动启用通道')}
size='default'
checkedText=''
uncheckedText=''
@@ -143,7 +146,7 @@ export default function SettingsMonitoring(props) {
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
保存监控设置
{t('保存监控设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsSensitiveWords(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
CheckSensitiveEnabled: false,
@@ -20,7 +22,7 @@ export default function SettingsSensitiveWords(props) {
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -39,13 +41,13 @@ export default function SettingsSensitiveWords(props) {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
@@ -71,12 +73,12 @@ export default function SettingsSensitiveWords(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'屏蔽词过滤设置'}>
<Form.Section text={t('屏蔽词过滤设置')}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'CheckSensitiveEnabled'}
label={'启用屏蔽词过滤功能'}
label={t('启用屏蔽词过滤功能')}
size='default'
checkedText=''
uncheckedText=''
@@ -91,7 +93,7 @@ export default function SettingsSensitiveWords(props) {
<Col span={8}>
<Form.Switch
field={'CheckSensitiveOnPromptEnabled'}
label={'启用 Prompt 检查'}
label={t('启用 Prompt 检查')}
size='default'
checkedText=''
uncheckedText=''
@@ -107,9 +109,9 @@ export default function SettingsSensitiveWords(props) {
<Row>
<Col span={16}>
<Form.TextArea
label={'屏蔽词列表'}
extraText={'一行一个屏蔽词,不需要符号分割'}
placeholder={'一行一个屏蔽词,不需要符号分割'}
label={t('屏蔽词列表')}
extraText={t('一行一个屏蔽词,不需要符号分割')}
placeholder={t('一行一个屏蔽词,不需要符号分割')}
field={'SensitiveWords'}
onChange={(value) =>
setInputs({
@@ -124,7 +126,7 @@ export default function SettingsSensitiveWords(props) {
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
保存屏蔽词过滤设置
{t('保存屏蔽词过滤设置')}
</Button>
</Row>
</Form.Section>

View File

@@ -1,19 +1,22 @@
import React, { useEffect, useState } from 'react';
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
import { useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import SystemSetting from '../../components/SystemSetting';
import { isRoot } from '../../helpers';
import OtherSetting from '../../components/OtherSetting';
import PersonalSetting from '../../components/PersonalSetting';
import OperationSetting from '../../components/OperationSetting';
const Setting = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const [tabActiveKey, setTabActiveKey] = useState('1');
let panes = [
{
tab: '个人设置',
tab: t('个人设置'),
content: <PersonalSetting />,
itemKey: 'personal',
},
@@ -21,17 +24,17 @@ const Setting = () => {
if (isRoot()) {
panes.push({
tab: '运营设置',
tab: t('运营设置'),
content: <OperationSetting />,
itemKey: 'operation',
});
panes.push({
tab: '系统设置',
tab: t('系统设置'),
content: <SystemSetting />,
itemKey: 'system',
});
panes.push({
tab: '其他设置',
tab: t('其他设置'),
content: <OtherSetting />,
itemKey: 'other',
});

View File

@@ -1,20 +1,24 @@
import React from 'react';
import TokensTable from '../../components/TokensTable';
import { Banner, Layout } from '@douyinfe/semi-ui';
const Token = () => (
<>
<Layout>
<Layout.Header>
import { useTranslation } from 'react-i18next';
const Token = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<Banner
type='warning'
description='令牌无法精确控制使用额度,请勿直接将令牌分发给用户。'
description={t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')}
/>
</Layout.Header>
<Layout.Content>
<TokensTable />
</Layout.Content>
</Layout>
</>
);
</Layout.Content>
</Layout>
</>
);
};
export default Token;

View File

@@ -21,8 +21,10 @@ import {
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const TopUp = () => {
const { t } = useTranslation();
const [redemptionCode, setRedemptionCode] = useState('');
const [topUpCode, setTopUpCode] = useState('');
const [topUpCount, setTopUpCount] = useState(0);
@@ -38,7 +40,7 @@ const TopUp = () => {
const topUp = async () => {
if (redemptionCode === '') {
showInfo('请输入兑换码!');
showInfo(t('请输入兑换码!'));
return;
}
setIsSubmitting(true);
@@ -48,10 +50,10 @@ const TopUp = () => {
});
const { success, message, data } = res.data;
if (success) {
showSuccess('兑换成功!');
showSuccess(t('兑换成功!'));
Modal.success({
title: '兑换成功!',
content: '成功兑换额度:' + renderQuota(data),
title: t('兑换成功!'),
content: t('成功兑换额度:') + renderQuota(data),
centered: true,
});
setUserQuota((quota) => {
@@ -62,7 +64,7 @@ const TopUp = () => {
showError(message);
}
} catch (err) {
showError('请求失败');
showError(t('请求失败'));
} finally {
setIsSubmitting(false);
}
@@ -70,7 +72,7 @@ const TopUp = () => {
const openTopUpLink = () => {
if (!topUpLink) {
showError('超级管理员未设置充值链接!');
showError(t('超级管理员未设置充值链接!'));
return;
}
window.open(topUpLink, '_blank');
@@ -78,12 +80,12 @@ const TopUp = () => {
const preTopUp = async (payment) => {
if (!enableOnlineTopUp) {
showError('管理员未开启在线充值!');
showError(t('管理员未开启在线充值!'));
return;
}
await getAmount();
if (topUpCount < minTopUp) {
showError('充值数量不能小于' + minTopUp);
showError(t('充值数量不能小于') + minTopUp);
return;
}
setPayWay(payment);
@@ -174,7 +176,7 @@ const TopUp = () => {
const renderAmount = () => {
// console.log(amount);
return amount + '元';
return amount + ' ' + t('元');
};
const getAmount = async (value) => {
@@ -214,11 +216,11 @@ const TopUp = () => {
<div>
<Layout>
<Layout.Header>
<h3>我的钱包</h3>
<h3>{t('我的钱包')}</h3>
</Layout.Header>
<Layout.Content>
<Modal
title='确定要充值吗'
title={t('确定要充值吗')}
visible={open}
onOk={onlineTopUp}
onCancel={handleCancel}
@@ -226,24 +228,24 @@ const TopUp = () => {
size={'small'}
centered={true}
>
<p>充值数量{topUpCount}</p>
<p>实付金额{renderAmount()}</p>
<p>是否确认充值</p>
<p>{t('充值数量')}{topUpCount}</p>
<p>{t('实付金额')}{renderAmount()}</p>
<p>{t('是否确认充值?')}</p>
</Modal>
<div
style={{ marginTop: 20, display: 'flex', justifyContent: 'center' }}
>
<Card style={{ width: '500px', padding: '20px' }}>
<Title level={3} style={{ textAlign: 'center' }}>
余额 {renderQuota(userQuota)}
{t('余额')} {renderQuota(userQuota)}
</Title>
<div style={{ marginTop: 20 }}>
<Divider>兑换余额</Divider>
<Divider>{t('兑换余额')}</Divider>
<Form>
<Form.Input
field={'redemptionCode'}
label={'兑换码'}
placeholder='兑换码'
label={t('兑换码')}
placeholder={t('兑换码')}
name='redemptionCode'
value={redemptionCode}
onChange={(value) => {
@@ -257,7 +259,7 @@ const TopUp = () => {
theme={'solid'}
onClick={openTopUpLink}
>
获取兑换码
{t('获取兑换码')}
</Button>
) : null}
<Button
@@ -266,21 +268,19 @@ const TopUp = () => {
onClick={topUp}
disabled={isSubmitting}
>
{isSubmitting ? '兑换中...' : '兑换'}
{isSubmitting ? t('兑换中...') : t('兑换')}
</Button>
</Space>
</Form>
</div>
<div style={{ marginTop: 20 }}>
<Divider>在线充值</Divider>
<Divider>{t('在线充值')}</Divider>
<Form>
<Form.Input
disabled={!enableOnlineTopUp}
field={'redemptionCount'}
label={'实付金额:' + renderAmount()}
placeholder={
'充值数量,最低 ' + renderQuotaWithAmount(minTopUp)
}
label={t('实付金额:') + ' ' + renderAmount()}
placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
name='redemptionCount'
type={'number'}
value={topUpCount}
@@ -300,7 +300,7 @@ const TopUp = () => {
preTopUp('zfb');
}}
>
支付宝
{t('支付宝')}
</Button>
<Button
style={{
@@ -312,7 +312,7 @@ const TopUp = () => {
preTopUp('wx');
}}
>
微信
{t('微信')}
</Button>
</Space>
</Form>

View File

@@ -14,6 +14,7 @@ import {
Spin,
Typography,
} from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const EditUser = (props) => {
const userId = props.editingUser.id;
@@ -120,11 +121,13 @@ const EditUser = (props) => {
setIsModalOpen(true);
};
const { t } = useTranslation();
return (
<>
<SideSheet
placement={'right'}
title={<Title level={3}>{'编辑用户'}</Title>}
title={<Title level={3}>{t('编辑用户')}</Title>}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
visible={props.visible}
@@ -132,7 +135,7 @@ const EditUser = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme='solid' size={'large'} onClick={submit}>
提交
{t('提交')}
</Button>
<Button
theme='solid'
@@ -140,7 +143,7 @@ const EditUser = (props) => {
type={'tertiary'}
onClick={handleCancel}
>
取消
{t('取消')}
</Button>
</Space>
</div>
@@ -151,35 +154,35 @@ const EditUser = (props) => {
>
<Spin spinning={loading}>
<div style={{ marginTop: 20 }}>
<Typography.Text>用户名</Typography.Text>
<Typography.Text>{t('用户名')}</Typography.Text>
</div>
<Input
label='用户名'
label={t('用户名')}
name='username'
placeholder={'请输入新的用户名'}
placeholder={t('请输入新的用户名')}
onChange={(value) => handleInputChange('username', value)}
value={username}
autoComplete='new-password'
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>密码</Typography.Text>
<Typography.Text>{t('密码')}</Typography.Text>
</div>
<Input
label='密码'
label={t('密码')}
name='password'
type={'password'}
placeholder={'请输入新的密码,最短 8 位'}
placeholder={t('请输入新的密码,最短 8 位')}
onChange={(value) => handleInputChange('password', value)}
value={password}
autoComplete='new-password'
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>显示名称</Typography.Text>
<Typography.Text>{t('显示名称')}</Typography.Text>
</div>
<Input
label='显示名称'
label={t('显示名称')}
name='display_name'
placeholder={'请输入新的显示名称'}
placeholder={t('请输入新的显示名称')}
onChange={(value) => handleInputChange('display_name', value)}
value={display_name}
autoComplete='new-password'
@@ -187,76 +190,76 @@ const EditUser = (props) => {
{userId && (
<>
<div style={{ marginTop: 20 }}>
<Typography.Text>分组</Typography.Text>
<Typography.Text>{t('分组')}</Typography.Text>
</div>
<Select
placeholder={'请选择分组'}
placeholder={t('请选择分组')}
name='group'
fluid
search
selection
allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
onChange={(value) => handleInputChange('group', value)}
value={inputs.group}
autoComplete='new-password'
optionList={groupOptions}
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
<Typography.Text>{`${t('剩余额度')}${renderQuotaWithPrompt(quota)}`}</Typography.Text>
</div>
<Space>
<Input
name='quota'
placeholder={'请输入新的剩余额度'}
placeholder={t('请输入新的剩余额度')}
onChange={(value) => handleInputChange('quota', value)}
value={quota}
type={'number'}
autoComplete='new-password'
/>
<Button onClick={openAddQuotaModal}>添加额度</Button>
<Button onClick={openAddQuotaModal}>{t('添加额度')}</Button>
</Space>
</>
)}
<Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider>
<Divider style={{ marginTop: 20 }}>{t('以下信息不可修改')}</Divider>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的 GitHub 账户</Typography.Text>
<Typography.Text>{t('已绑定的 GitHub 账户')}</Typography.Text>
</div>
<Input
name='github_id'
value={github_id}
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的微信账户</Typography.Text>
<Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
</div>
<Input
name='wechat_id'
value={wechat_id}
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的邮箱账户</Typography.Text>
<Typography.Text>{t('已绑定的邮箱账户')}</Typography.Text>
</div>
<Input
name='email'
value={email}
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的Telegram账户</Typography.Text>
<Typography.Text>{t('已绑定的Telegram账户')}</Typography.Text>
</div>
<Input
name='telegram_id'
value={telegram_id}
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly
/>
</Spin>
@@ -272,11 +275,11 @@ const EditUser = (props) => {
closable={null}
>
<div style={{ marginTop: 20 }}>
<Typography.Text>{`新额度${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal))}`}</Typography.Text>
<Typography.Text>{`${t('新额度')}${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal))}`}</Typography.Text>
</div>
<Input
name='addQuotaLocal'
placeholder={'需要添加的额度(支持负数)'}
placeholder={t('需要添加的额度(支持负数)')}
onChange={(value) => {
setAddQuotaLocal(value);
}}

View File

@@ -1,18 +1,22 @@
import React from 'react';
import UsersTable from '../../components/UsersTable';
import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const User = () => (
<>
<Layout>
<Layout.Header>
<h3>管理用户</h3>
const User = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<h3>{t('管理用户')}</h3>
</Layout.Header>
<Layout.Content>
<UsersTable />
</Layout.Content>
</Layout>
</>
);
</Layout.Content>
</Layout>
</>
);
};
export default User;