feat: support i18n
feat: support i18n
This commit is contained in:
21
i18n/en.json
21
i18n/en.json
@@ -174,11 +174,11 @@
|
||||
"\"验证码\"": "\"Verification code\"",
|
||||
"全部用户": "All users",
|
||||
"当前用户": "Current user",
|
||||
"'全部'": "'All'",
|
||||
"'充值'": "'Recharge'",
|
||||
"'消费'": "'Consumption'",
|
||||
"'管理'": "'Management'",
|
||||
"'系统'": "'System'",
|
||||
"全部'": "All'",
|
||||
"充值'": "Recharge'",
|
||||
"消费'": "Consumption'",
|
||||
"管理'": "Management'",
|
||||
"系统'": "System'",
|
||||
" 充值 ": " Recharge ",
|
||||
" 消费 ": " Consumption ",
|
||||
" 管理 ": " Management ",
|
||||
@@ -377,6 +377,7 @@
|
||||
"添加新的用户": "Add New User",
|
||||
"自定义": "Custom",
|
||||
"等价金额": "Equivalent Amount",
|
||||
"等价金额:{{quota}}:": "Equivalent amount: {{quota}}",
|
||||
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
|
||||
"请求次数过多,请稍后再试": "Too many requests, please try again later",
|
||||
"服务器内部错误,请联系管理员": "Server internal error, please contact the administrator",
|
||||
@@ -525,5 +526,13 @@
|
||||
"模型版本": "Model version",
|
||||
"请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
|
||||
"点击查看": "click to view",
|
||||
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!"
|
||||
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!",
|
||||
"第 {{start}} - {{end}} 条,共 {{total}} 条": "Items {{start}} - {{end}} of {{total}}",
|
||||
"模型测试": "Model test",
|
||||
"请选择最长响应时间": "Please select the longest response time",
|
||||
"成功时自动启用通道": "Enable channel when successful",
|
||||
"分钟": "minutes",
|
||||
"设置过短会影响数据库性能": "Setting too short will affect database performance",
|
||||
"仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour",
|
||||
"当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
"react-turnstile": "^1.0.5",
|
||||
"semantic-ui-offline": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"sse": "github:mpetazzoni/sse.js"
|
||||
"sse": "github:mpetazzoni/sse.js",
|
||||
"i18next": "^23.16.8",
|
||||
"react-i18next": "^13.0.0",
|
||||
"i18next-browser-languagedetector": "^7.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
58
web/pnpm-lock.yaml
generated
58
web/pnpm-lock.yaml
generated
@@ -32,6 +32,12 @@ importers:
|
||||
history:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
i18next:
|
||||
specifier: ^23.16.8
|
||||
version: 23.16.8
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.2
|
||||
marked:
|
||||
specifier: ^4.1.1
|
||||
version: 4.3.0
|
||||
@@ -47,6 +53,9 @@ importers:
|
||||
react-fireworks:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
react-i18next:
|
||||
specifier: ^13.0.0
|
||||
version: 13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.3.0
|
||||
version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -1071,6 +1080,15 @@ packages:
|
||||
history@5.3.0:
|
||||
resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
i18next-browser-languagedetector@7.2.2:
|
||||
resolution: {integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==}
|
||||
|
||||
i18next@23.16.8:
|
||||
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1487,6 +1505,19 @@ packages:
|
||||
react-fireworks@1.0.4:
|
||||
resolution: {integrity: sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw==}
|
||||
|
||||
react-i18next@13.5.0:
|
||||
resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -1812,6 +1843,10 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
@@ -2935,6 +2970,18 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
i18next-browser-languagedetector@7.2.2:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
i18next@23.16.8:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -3611,6 +3658,15 @@ snapshots:
|
||||
|
||||
react-fireworks@1.0.4: {}
|
||||
|
||||
react-i18next@13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 23.16.8
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@18.3.1: {}
|
||||
@@ -3998,6 +4054,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
@@ -26,6 +26,7 @@ import Pricing from './pages/Pricing/index.js';
|
||||
import Task from "./pages/Task/index.js";
|
||||
import Playground from './pages/Playground/Playground.js';
|
||||
import OAuth2Callback from "./components/OAuth2Callback.js";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
@@ -34,6 +35,7 @@ const About = lazy(() => import('./pages/About'));
|
||||
function App() {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
// const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const loadUser = () => {
|
||||
let user = localStorage.getItem('user');
|
||||
@@ -56,7 +58,12 @@ function App() {
|
||||
linkElement.href = logo;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
// 从localStorage获取上次使用的语言
|
||||
const savedLang = localStorage.getItem('i18nextLng');
|
||||
if (savedLang) {
|
||||
i18n.changeLanguage(savedLang);
|
||||
}
|
||||
}, [i18n]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18next from 'i18next';
|
||||
import { Tag } from '@douyinfe/semi-ui';
|
||||
|
||||
export function renderText(text, limit) {
|
||||
@@ -16,7 +17,7 @@ export function renderGroup(group) {
|
||||
if (group === '') {
|
||||
return (
|
||||
<Tag size='large' key='default' color='orange'>
|
||||
用户分组
|
||||
{i18next.t('用户分组')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -144,14 +145,16 @@ export function renderModelPrice(
|
||||
completionRatio,
|
||||
groupRatio,
|
||||
) {
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
return '模型价格:$' + modelPrice + ' * 分组倍率:' + groupRatio + ' = $' + modelPrice * groupRatio;
|
||||
return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
|
||||
price: modelPrice,
|
||||
ratio: groupRatio,
|
||||
total: modelPrice * groupRatio
|
||||
});
|
||||
} else {
|
||||
if (completionRatio === undefined) {
|
||||
completionRatio = 0;
|
||||
}
|
||||
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
||||
let inputRatioPrice = modelRatio * 2.0;
|
||||
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
||||
let price =
|
||||
@@ -160,15 +163,28 @@ export function renderModelPrice(
|
||||
return (
|
||||
<>
|
||||
<article>
|
||||
<p>提示:${inputRatioPrice} * {groupRatio} = ${inputRatioPrice * groupRatio} / 1M tokens</p>
|
||||
<p>补全:${completionRatioPrice} * {groupRatio} = ${completionRatioPrice * groupRatio} / 1M tokens</p>
|
||||
<p>{i18next.t('提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
|
||||
price: inputRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: inputRatioPrice * groupRatio
|
||||
})}</p>
|
||||
<p>{i18next.t('补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
|
||||
price: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: completionRatioPrice * groupRatio
|
||||
})}</p>
|
||||
<p></p>
|
||||
<p>
|
||||
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
|
||||
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} * 分组 {groupRatio} =
|
||||
${price.toFixed(6)}
|
||||
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6)
|
||||
})}
|
||||
</p>
|
||||
<p>仅供参考,以实际扣费为准</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
@@ -180,11 +196,16 @@ export function renderModelPriceSimple(
|
||||
modelPrice = -1,
|
||||
groupRatio,
|
||||
) {
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
return '价格:$' + modelPrice + ' * 分组:' + groupRatio;
|
||||
return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
|
||||
price: modelPrice,
|
||||
ratio: groupRatio
|
||||
});
|
||||
} else {
|
||||
return '模型: ' + modelRatio + ' * 分组: ' + groupRatio;
|
||||
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
|
||||
ratio: modelRatio,
|
||||
groupRatio: groupRatio
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +245,12 @@ export function renderAudioModelPrice(
|
||||
<p>音频补全:${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p>
|
||||
<p></p>
|
||||
<p>
|
||||
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
|
||||
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} +
|
||||
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio}
|
||||
@@ -245,7 +270,7 @@ export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
return '|' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -315,7 +340,7 @@ export const modelColorMap = {
|
||||
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
|
||||
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
|
||||
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
|
||||
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃色
|
||||
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃<EFBFBD><EFBFBD><EFBFBD>
|
||||
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
|
||||
'gpt-4': 'rgb(135,206,235)', // 天蓝色
|
||||
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
|
||||
@@ -338,7 +363,7 @@ export const modelColorMap = {
|
||||
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
|
||||
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
|
||||
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
|
||||
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
|
||||
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(<EFBFBD><EFBFBD><EFBFBD>Babbage相同,表示同一类功能)
|
||||
'tts-1': 'rgb(255,140,0)', // 深橙色
|
||||
'tts-1-1106': 'rgb(255,165,0)', // 橙色
|
||||
'tts-1-hd': 'rgb(255,215,0)', // 金色
|
||||
|
||||
26
web/src/i18n/i18n.js
Normal file
26
web/src/i18n/i18n.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation
|
||||
}
|
||||
},
|
||||
fallbackLng: 'zh',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
1238
web/src/i18n/locales/en copy.json
Normal file
1238
web/src/i18n/locales/en copy.json
Normal file
File diff suppressed because it is too large
Load Diff
1213
web/src/i18n/locales/en.json
Normal file
1213
web/src/i18n/locales/en.json
Normal file
File diff suppressed because it is too large
Load Diff
13
web/src/i18n/locales/zh.json
Normal file
13
web/src/i18n/locales/zh.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"首页": "首页",
|
||||
"控制台": "控制台",
|
||||
"定价": "定价",
|
||||
"关于": "关于",
|
||||
"登录": "登录",
|
||||
"注册": "注册",
|
||||
"退出": "退出",
|
||||
"语言": "语言",
|
||||
"展开侧边栏": "展开侧边栏",
|
||||
"关闭侧边栏": "关闭侧边栏",
|
||||
"注销成功!": "注销成功!"
|
||||
}
|
||||
@@ -25,17 +25,28 @@ body {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.panel-desc-card {
|
||||
/*min-width: 320px;*/
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
/*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/
|
||||
/* padding: 0 0;*/
|
||||
/*}*/
|
||||
.topnav .semi-navigation-list-wrapper {
|
||||
max-width: calc(55vw - 20px);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li {
|
||||
padding: 0 0;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.semi-navigation-footer {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.semi-table-tbody,
|
||||
.semi-table-row,
|
||||
.semi-table-row-cell {
|
||||
@@ -105,11 +116,6 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.semi-navigation-vertical {
|
||||
/*display: flex;*/
|
||||
/*flex-direction: column;*/
|
||||
}
|
||||
|
||||
.semi-navigation-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ThemeProvider } from './context/Theme';
|
||||
import FooterBar from './components/Footer';
|
||||
import { StyleProvider } from './context/Style/index.js';
|
||||
import PageLayout from './components/PageLayout.js';
|
||||
import './i18n/i18n.js';
|
||||
|
||||
// initialization
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,6 +26,7 @@ export default defineConfig({
|
||||
esbuildOptions: {
|
||||
loader: {
|
||||
'.js': 'jsx',
|
||||
'.json': 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -45,6 +46,7 @@ export default defineConfig({
|
||||
'react-toastify',
|
||||
'react-turnstile',
|
||||
],
|
||||
'i18n': ['i18next', 'react-i18next', 'i18next-browser-languagedetector'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user