mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-17 09:16:36 +08:00
feat: i18n support
This commit is contained in:
parent
e7ea7c866f
commit
d0965050a9
@ -667,6 +667,64 @@
|
||||
"save": "Save General Settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"notice": {
|
||||
"title": "Notice Settings",
|
||||
"content": "Notice Content",
|
||||
"content_placeholder": "Enter new notice content here, supports Markdown & HTML code",
|
||||
"buttons": {
|
||||
"save": "Save Notice"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "System Settings",
|
||||
"name": "System Name",
|
||||
"name_placeholder": "Please enter system name",
|
||||
"logo": "Logo Image URL",
|
||||
"logo_placeholder": "Enter Logo image URL here",
|
||||
"theme": {
|
||||
"title": "Theme Name",
|
||||
"link": "Available Themes",
|
||||
"placeholder": "Please enter theme name"
|
||||
},
|
||||
"buttons": {
|
||||
"save_name": "Set System Name",
|
||||
"save_logo": "Set Logo",
|
||||
"save_theme": "Set Theme (Restart Required)"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"title": "Content Settings",
|
||||
"homepage": {
|
||||
"title": "Homepage Content",
|
||||
"placeholder": "Enter homepage content here, supports Markdown & HTML code. Status information will not be shown after setting. If a link is entered, it will be used as the src attribute of an iframe, allowing you to set any webpage as homepage."
|
||||
},
|
||||
"about": {
|
||||
"title": "About System",
|
||||
"description": "You can set about content in settings page, supports HTML & Markdown",
|
||||
"repository": "Project Repository:",
|
||||
"loading_failed": "Failed to load about content..."
|
||||
},
|
||||
"footer": {
|
||||
"title": "Footer",
|
||||
"placeholder": "Enter new footer here, leave empty to use default footer, supports HTML code"
|
||||
},
|
||||
"buttons": {
|
||||
"save_homepage": "Save Homepage Content",
|
||||
"save_about": "Save About",
|
||||
"save_footer": "Set Footer"
|
||||
}
|
||||
},
|
||||
"copyright": {
|
||||
"notice": "Removing One API's copyright notice requires authorization. Project maintenance requires significant effort, if this project is meaningful to you, please actively support it."
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"built_by": "built by",
|
||||
"built_by_name": "JustSong",
|
||||
"license": ", source code is licensed under the",
|
||||
"mit": "MIT License"
|
||||
}
|
||||
}
|
||||
|
@ -667,6 +667,68 @@
|
||||
"save": "保存通用设置"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"notice": {
|
||||
"title": "公告设置",
|
||||
"content": "公告内容",
|
||||
"content_placeholder": "在此输入新的公告内容,支持 Markdown & HTML 代码",
|
||||
"buttons": {
|
||||
"save": "保存公告"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "系统设置",
|
||||
"name": "系统名称",
|
||||
"name_placeholder": "请输入系统名称",
|
||||
"logo": "Logo 图片地址",
|
||||
"logo_placeholder": "在此输入 Logo 图片地址",
|
||||
"theme": {
|
||||
"title": "主题名称",
|
||||
"link": "当前可用主题",
|
||||
"placeholder": "请输入主题名称"
|
||||
},
|
||||
"buttons": {
|
||||
"save_name": "设置系统名称",
|
||||
"save_logo": "设置 Logo",
|
||||
"save_theme": "设置主题(重启生效)"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"title": "内容设置",
|
||||
"homepage": {
|
||||
"title": "首页内容",
|
||||
"placeholder": "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。"
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"placeholder": "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。"
|
||||
},
|
||||
"footer": {
|
||||
"title": "页脚",
|
||||
"placeholder": "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码"
|
||||
},
|
||||
"buttons": {
|
||||
"save_homepage": "保存首页内容",
|
||||
"save_about": "保存关于",
|
||||
"save_footer": "设置页脚"
|
||||
}
|
||||
},
|
||||
"copyright": {
|
||||
"notice": "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "关于系统",
|
||||
"description": "可在设置页面设置关于内容,支持 HTML & Markdown",
|
||||
"repository": "项目仓库地址:",
|
||||
"loading_failed": "加载关于内容失败..."
|
||||
},
|
||||
"footer": {
|
||||
"built_by": "由",
|
||||
"built_by_name": "JustSong",
|
||||
"license": "构建,源代码遵循",
|
||||
"mit": "MIT 协议"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Container, Segment } from 'semantic-ui-react';
|
||||
import { getFooterHTML, getSystemName } from '../helpers';
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation();
|
||||
const systemName = getSystemName();
|
||||
const [footer, setFooter] = useState(getFooterHTML());
|
||||
let remainCheckTimes = 5;
|
||||
@ -40,13 +41,13 @@ const Footer = () => {
|
||||
<a href='https://github.com/songquanpeng/one-api' target='_blank'>
|
||||
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||
</a>
|
||||
由{' '}
|
||||
{t('footer.built_by')}{' '}
|
||||
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||
JustSong
|
||||
{t('footer.built_by_name')}
|
||||
</a>{' '}
|
||||
构建,源代码遵循{' '}
|
||||
{t('footer.license')}{' '}
|
||||
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||
MIT 协议
|
||||
{t('footer.mit')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,7 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
|
||||
import { API, showError, showSuccess, timestamp2string, verifyJSON } from '../helpers';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
verifyJSON,
|
||||
} from '../helpers';
|
||||
|
||||
const OperationSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -25,11 +31,13 @@ const OperationSetting = () => {
|
||||
DisplayInCurrencyEnabled: '',
|
||||
DisplayTokenStatEnabled: '',
|
||||
ApproximateTokenEnabled: '',
|
||||
RetryTimes: 0
|
||||
RetryTimes: 0,
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago
|
||||
let [historyTimestamp, setHistoryTimestamp] = useState(
|
||||
timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)
|
||||
); // a month ago
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
@ -37,7 +45,11 @@ const OperationSetting = () => {
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key === 'ModelRatio' || item.key === 'GroupRatio' || item.key === 'CompletionRatio') {
|
||||
if (
|
||||
item.key === 'ModelRatio' ||
|
||||
item.key === 'GroupRatio' ||
|
||||
item.key === 'CompletionRatio'
|
||||
) {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
if (item.value === '{}') {
|
||||
@ -63,7 +75,7 @@ const OperationSetting = () => {
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@ -85,11 +97,22 @@ const OperationSetting = () => {
|
||||
const submitConfig = async (group) => {
|
||||
switch (group) {
|
||||
case 'monitor':
|
||||
if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
|
||||
await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
|
||||
if (
|
||||
originInputs['ChannelDisableThreshold'] !==
|
||||
inputs.ChannelDisableThreshold
|
||||
) {
|
||||
await updateOption(
|
||||
'ChannelDisableThreshold',
|
||||
inputs.ChannelDisableThreshold
|
||||
);
|
||||
}
|
||||
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
|
||||
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
|
||||
if (
|
||||
originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
|
||||
) {
|
||||
await updateOption(
|
||||
'QuotaRemindThreshold',
|
||||
inputs.QuotaRemindThreshold
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ratio':
|
||||
@ -148,7 +171,9 @@ const OperationSetting = () => {
|
||||
|
||||
const deleteHistoryLogs = async () => {
|
||||
console.log(inputs);
|
||||
const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`);
|
||||
const res = await API.delete(
|
||||
`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`
|
||||
);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(`${data} 条日志已清理!`);
|
||||
@ -161,9 +186,7 @@ const OperationSetting = () => {
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>
|
||||
{t('setting.operation.quota.title')}
|
||||
</Header>
|
||||
<Header as='h3'>{t('setting.operation.quota.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label={t('setting.operation.quota.new_user')}
|
||||
@ -193,7 +216,9 @@ const OperationSetting = () => {
|
||||
value={inputs.QuotaForInviter}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t('setting.operation.quota.inviter_reward_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.quota.inviter_reward_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.quota.invitee_reward')}
|
||||
@ -203,18 +228,20 @@ const OperationSetting = () => {
|
||||
value={inputs.QuotaForInvitee}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t('setting.operation.quota.invitee_reward_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.quota.invitee_reward_placeholder'
|
||||
)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('quota').then();
|
||||
}}>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('quota').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.quota.buttons.save')}
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
{t('setting.operation.ratio.title')}
|
||||
</Header>
|
||||
<Header as='h3'>{t('setting.operation.ratio.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label={t('setting.operation.ratio.model.title')}
|
||||
@ -248,9 +275,11 @@ const OperationSetting = () => {
|
||||
placeholder={t('setting.operation.ratio.group.placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('ratio').then();
|
||||
}}>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('ratio').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.ratio.buttons.save')}
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
@ -274,9 +303,11 @@ const OperationSetting = () => {
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
deleteHistoryLogs().then();
|
||||
}}>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
deleteHistoryLogs().then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.log.buttons.clean')}
|
||||
</Form.Button>
|
||||
|
||||
@ -291,7 +322,9 @@ const OperationSetting = () => {
|
||||
value={inputs.ChannelDisableThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t('setting.operation.monitor.max_response_time_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.monitor.max_response_time_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.monitor.quota_reminder')}
|
||||
@ -301,7 +334,9 @@ const OperationSetting = () => {
|
||||
value={inputs.QuotaRemindThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t('setting.operation.monitor.quota_reminder_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.monitor.quota_reminder_placeholder'
|
||||
)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
@ -318,9 +353,11 @@ const OperationSetting = () => {
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
}}>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.monitor.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
@ -334,7 +371,9 @@ const OperationSetting = () => {
|
||||
autoComplete='new-password'
|
||||
value={inputs.TopUpLink}
|
||||
type='link'
|
||||
placeholder={t('setting.operation.general.topup_link_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.general.topup_link_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.general.chat_link')}
|
||||
@ -353,7 +392,9 @@ const OperationSetting = () => {
|
||||
value={inputs.QuotaPerUnit}
|
||||
type='number'
|
||||
step='0.01'
|
||||
placeholder={t('setting.operation.general.quota_per_unit_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.general.quota_per_unit_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.general.retry_times')}
|
||||
@ -364,7 +405,9 @@ const OperationSetting = () => {
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.RetryTimes}
|
||||
placeholder={t('setting.operation.general.retry_times_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.operation.general.retry_times_placeholder'
|
||||
)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
@ -387,9 +430,11 @@ const OperationSetting = () => {
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('general').then();
|
||||
}}>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('general').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.general.buttons.save')}
|
||||
</Form.Button>
|
||||
</Form>
|
||||
|
@ -1,10 +1,20 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Header,
|
||||
Message,
|
||||
Modal,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { API, showError, showSuccess, verifyJSON } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const OtherSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
Footer: '',
|
||||
Notice: '',
|
||||
@ -12,13 +22,13 @@ const OtherSetting = () => {
|
||||
SystemName: '',
|
||||
Logo: '',
|
||||
HomePageContent: '',
|
||||
Theme: ''
|
||||
Theme: '',
|
||||
});
|
||||
let [loading, setLoading] = useState(false);
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||
const [updateData, setUpdateData] = useState({
|
||||
tag_name: '',
|
||||
content: ''
|
||||
content: '',
|
||||
});
|
||||
|
||||
const getOptions = async () => {
|
||||
@ -45,7 +55,7 @@ const OtherSetting = () => {
|
||||
setLoading(true);
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@ -64,10 +74,6 @@ const OtherSetting = () => {
|
||||
await updateOption('Notice', inputs.Notice);
|
||||
};
|
||||
|
||||
const submitFooter = async () => {
|
||||
await updateOption('Footer', inputs.Footer);
|
||||
};
|
||||
|
||||
const submitSystemName = async () => {
|
||||
await updateOption('SystemName', inputs.SystemName);
|
||||
};
|
||||
@ -89,8 +95,7 @@ const OtherSetting = () => {
|
||||
};
|
||||
|
||||
const openGitHubRelease = () => {
|
||||
window.location =
|
||||
'https://github.com/songquanpeng/one-api/releases/latest';
|
||||
window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
|
||||
};
|
||||
|
||||
const checkUpdate = async () => {
|
||||
@ -103,7 +108,7 @@ const OtherSetting = () => {
|
||||
} else {
|
||||
setUpdateData({
|
||||
tag_name: tag_name,
|
||||
content: marked.parse(body)
|
||||
content: marked.parse(body),
|
||||
});
|
||||
setShowUpdateModal(true);
|
||||
}
|
||||
@ -113,87 +118,110 @@ const OtherSetting = () => {
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
|
||||
<Header as='h3'>{t('setting.other.notice.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='公告'
|
||||
placeholder='在此输入新的公告内容,支持 Markdown & HTML 代码'
|
||||
label={t('setting.other.notice.content')}
|
||||
placeholder={t('setting.other.notice.content_placeholder')}
|
||||
value={inputs.Notice}
|
||||
name='Notice'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
style={{ minHeight: 100, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
||||
<Form.Button onClick={submitNotice}>
|
||||
{t('setting.other.notice.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>个性化设置</Header>
|
||||
<Header as='h3'>{t('setting.other.system.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='系统名称'
|
||||
placeholder='在此输入系统名称'
|
||||
label={t('setting.other.system.name')}
|
||||
placeholder={t('setting.other.system.name_placeholder')}
|
||||
value={inputs.SystemName}
|
||||
name='SystemName'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitSystemName}>设置系统名称</Form.Button>
|
||||
<Form.Button onClick={submitSystemName}>
|
||||
{t('setting.other.system.buttons.save_name')}
|
||||
</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label={<label>主题名称(<Link
|
||||
to='https://github.com/songquanpeng/one-api/blob/main/web/README.md'>当前可用主题</Link>)</label>}
|
||||
placeholder='请输入主题名称'
|
||||
label={
|
||||
<label>
|
||||
{t('setting.other.system.theme.title')}(
|
||||
<Link to='https://github.com/songquanpeng/one-api/blob/main/web/README.md'>
|
||||
{t('setting.other.system.theme.link')}
|
||||
</Link>
|
||||
)
|
||||
</label>
|
||||
}
|
||||
placeholder={t('setting.other.system.theme.placeholder')}
|
||||
value={inputs.Theme}
|
||||
name='Theme'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitTheme}>设置主题(重启生效)</Form.Button>
|
||||
<Form.Button onClick={submitTheme}>
|
||||
{t('setting.other.system.buttons.save_theme')}
|
||||
</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='Logo 图片地址'
|
||||
placeholder='在此输入 Logo 图片地址'
|
||||
label={t('setting.other.system.logo')}
|
||||
placeholder={t('setting.other.system.logo_placeholder')}
|
||||
value={inputs.Logo}
|
||||
name='Logo'
|
||||
type='url'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitLogo}>设置 Logo</Form.Button>
|
||||
<Form.Button onClick={submitLogo}>
|
||||
{t('setting.other.system.buttons.save_logo')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>{t('setting.other.content.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='首页内容'
|
||||
placeholder='在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
|
||||
label={t('setting.other.content.homepage.title')}
|
||||
placeholder={t('setting.other.content.homepage.placeholder')}
|
||||
value={inputs.HomePageContent}
|
||||
name='HomePageContent'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button>
|
||||
<Form.Button onClick={() => submitOption('HomePageContent')}>
|
||||
{t('setting.other.content.buttons.save_homepage')}
|
||||
</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='关于'
|
||||
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
|
||||
label={t('setting.other.content.about.title')}
|
||||
placeholder={t('setting.other.content.about.placeholder')}
|
||||
value={inputs.About}
|
||||
name='About'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitAbout}>保存关于</Form.Button>
|
||||
<Message>移除 One API
|
||||
的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。</Message>
|
||||
<Form.Button onClick={submitAbout}>
|
||||
{t('setting.other.content.buttons.save_about')}
|
||||
</Form.Button>
|
||||
<Message>{t('setting.other.copyright.notice')}</Message>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='页脚'
|
||||
placeholder='在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
|
||||
label={t('setting.other.content.footer.title')}
|
||||
placeholder={t('setting.other.content.footer.placeholder')}
|
||||
value={inputs.Footer}
|
||||
name='Footer'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitFooter}>设置页脚</Form.Button>
|
||||
<Form.Button onClick={() => submitOption('Footer')}>
|
||||
{t('setting.other.content.buttons.save_footer')}
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
<Modal
|
||||
|
@ -1,8 +1,23 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Header,
|
||||
Image,
|
||||
Message,
|
||||
Modal,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
showError,
|
||||
showInfo,
|
||||
showNotice,
|
||||
showSuccess,
|
||||
} from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../context/User';
|
||||
import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils';
|
||||
@ -16,7 +31,7 @@ const PersonalSetting = () => {
|
||||
wechat_verification_code: '',
|
||||
email_verification_code: '',
|
||||
email: '',
|
||||
self_account_deletion_confirmation: ''
|
||||
self_account_deletion_confirmation: '',
|
||||
});
|
||||
const [status, setStatus] = useState({});
|
||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||
@ -28,8 +43,8 @@ const PersonalSetting = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [affLink, setAffLink] = useState("");
|
||||
const [systemToken, setSystemToken] = useState("");
|
||||
const [affLink, setAffLink] = useState('');
|
||||
const [systemToken, setSystemToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
@ -65,7 +80,7 @@ const PersonalSetting = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setSystemToken(data);
|
||||
setAffLink("");
|
||||
setAffLink('');
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板`);
|
||||
} else {
|
||||
@ -79,7 +94,7 @@ const PersonalSetting = () => {
|
||||
if (success) {
|
||||
let link = `${window.location.origin}/register?aff=${data}`;
|
||||
setAffLink(link);
|
||||
setSystemToken("");
|
||||
setSystemToken('');
|
||||
await copy(link);
|
||||
showSuccess(`邀请链接已复制到剪切板`);
|
||||
} else {
|
||||
@ -172,9 +187,7 @@ const PersonalSetting = () => {
|
||||
return (
|
||||
<div style={{ lineHeight: '40px' }}>
|
||||
<Header as='h3'>{t('setting.personal.general.title')}</Header>
|
||||
<Message>
|
||||
{t('setting.personal.general.system_token_notice')}
|
||||
</Message>
|
||||
<Message>{t('setting.personal.general.system_token_notice')}</Message>
|
||||
<Button as={Link} to={`/user/edit/`}>
|
||||
{t('setting.personal.general.buttons.update_profile')}
|
||||
</Button>
|
||||
@ -184,9 +197,11 @@ const PersonalSetting = () => {
|
||||
<Button onClick={getAffLink}>
|
||||
{t('setting.personal.general.buttons.copy_invite')}
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
{t('setting.personal.general.buttons.delete_account')}
|
||||
</Button>
|
||||
|
||||
@ -230,7 +245,9 @@ const PersonalSetting = () => {
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={t('setting.personal.binding.wechat.verification_code')}
|
||||
placeholder={t(
|
||||
'setting.personal.binding.wechat.verification_code'
|
||||
)}
|
||||
name='wechat_verification_code'
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={handleInputChange}
|
||||
@ -268,21 +285,30 @@ const PersonalSetting = () => {
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={t('setting.personal.binding.email.email_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.personal.binding.email.email_placeholder'
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||
<Button
|
||||
onClick={sendVerificationCode}
|
||||
disabled={disableButton || loading}
|
||||
>
|
||||
{disableButton
|
||||
? t('setting.personal.binding.email.get_code_retry', { countdown })
|
||||
? t('setting.personal.binding.email.get_code_retry', {
|
||||
countdown,
|
||||
})
|
||||
: t('setting.personal.binding.email.get_code')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={t('setting.personal.binding.email.code_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.personal.binding.email.code_placeholder'
|
||||
)}
|
||||
name='email_verification_code'
|
||||
value={inputs.email_verification_code}
|
||||
onChange={handleInputChange}
|
||||
@ -295,7 +321,13 @@ const PersonalSetting = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color=''
|
||||
fluid
|
||||
@ -325,16 +357,21 @@ const PersonalSetting = () => {
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>{t('setting.personal.delete_account.title')}</Modal.Header>
|
||||
<Modal.Header>
|
||||
{t('setting.personal.delete_account.title')}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Message>{t('setting.personal.delete_account.warning')}</Message>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={t('setting.personal.delete_account.confirm_placeholder', {
|
||||
username: userState?.user?.username
|
||||
})}
|
||||
placeholder={t(
|
||||
'setting.personal.delete_account.confirm_placeholder',
|
||||
{
|
||||
username: userState?.user?.username,
|
||||
}
|
||||
)}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={handleInputChange}
|
||||
@ -347,7 +384,13 @@ const PersonalSetting = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color='red'
|
||||
fluid
|
||||
|
@ -1,6 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Header,
|
||||
Modal,
|
||||
Message,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, removeTrailingSlash, showError } from '../helpers';
|
||||
|
||||
const SystemSetting = () => {
|
||||
@ -33,13 +41,14 @@ const SystemSetting = () => {
|
||||
TurnstileSecretKey: '',
|
||||
RegisterEnabled: '',
|
||||
EmailDomainRestrictionEnabled: '',
|
||||
EmailDomainWhitelist: ''
|
||||
EmailDomainWhitelist: '',
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
||||
const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
|
||||
const [showPasswordWarningModal, setShowPasswordWarningModal] =
|
||||
useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
@ -51,13 +60,15 @@ const SystemSetting = () => {
|
||||
});
|
||||
setInputs({
|
||||
...newInputs,
|
||||
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
|
||||
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
|
||||
});
|
||||
setOriginInputs(newInputs);
|
||||
|
||||
setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||
return { key: item, text: item, value: item };
|
||||
}));
|
||||
setEmailDomainWhitelist(
|
||||
newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||
return { key: item, text: item, value: item };
|
||||
})
|
||||
);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -85,7 +96,7 @@ const SystemSetting = () => {
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@ -93,7 +104,8 @@ const SystemSetting = () => {
|
||||
value = value.split(',');
|
||||
}
|
||||
setInputs((inputs) => ({
|
||||
...inputs, [key]: value
|
||||
...inputs,
|
||||
[key]: value,
|
||||
}));
|
||||
} else {
|
||||
showError(message);
|
||||
@ -157,13 +169,16 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const submitEmailDomainWhitelist = async () => {
|
||||
if (
|
||||
originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
|
||||
originInputs['EmailDomainWhitelist'] !==
|
||||
inputs.EmailDomainWhitelist.join(',') &&
|
||||
inputs.SMTPToken !== ''
|
||||
) {
|
||||
await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
|
||||
await updateOption(
|
||||
'EmailDomainWhitelist',
|
||||
inputs.EmailDomainWhitelist.join(',')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -218,7 +233,7 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitLarkOAuth = async () => {
|
||||
const submitLarkOAuth = async () => {
|
||||
if (originInputs['LarkClientId'] !== inputs.LarkClientId) {
|
||||
await updateOption('LarkClientId', inputs.LarkClientId);
|
||||
}
|
||||
@ -244,19 +259,25 @@ const SystemSetting = () => {
|
||||
|
||||
const submitNewRestrictedDomain = () => {
|
||||
const localDomainList = inputs.EmailDomainWhitelist;
|
||||
if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
|
||||
if (
|
||||
restrictedDomainInput !== '' &&
|
||||
!localDomainList.includes(restrictedDomainInput)
|
||||
) {
|
||||
setRestrictedDomainInput('');
|
||||
setInputs({
|
||||
...inputs,
|
||||
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
|
||||
});
|
||||
setEmailDomainWhitelist([...EmailDomainWhitelist, {
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
}]);
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
@ -266,7 +287,9 @@ const SystemSetting = () => {
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label={t('setting.system.general.server_address')}
|
||||
placeholder={t('setting.system.general.server_address_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.system.general.server_address_placeholder'
|
||||
)}
|
||||
value={inputs.ServerAddress}
|
||||
name='ServerAddress'
|
||||
onChange={handleInputChange}
|
||||
@ -291,7 +314,9 @@ const SystemSetting = () => {
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>{t('setting.system.password_login.warning.title')}</Modal.Header>
|
||||
<Modal.Header>
|
||||
{t('setting.system.password_login.warning.title')}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p>{t('setting.system.password_login.warning.content')}</p>
|
||||
</Modal.Content>
|
||||
@ -364,21 +389,28 @@ const SystemSetting = () => {
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label={t('setting.system.email_restriction.add_domain')}
|
||||
placeholder={t('setting.system.email_restriction.add_domain_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.system.email_restriction.add_domain_placeholder'
|
||||
)}
|
||||
value={restrictedDomainInput}
|
||||
onChange={(e, { value }) => {
|
||||
setRestrictedDomainInput(value);
|
||||
}}
|
||||
action={
|
||||
<Button onClick={() => {
|
||||
if (restrictedDomainInput === '') return;
|
||||
setEmailDomainWhitelist([...EmailDomainWhitelist, {
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput
|
||||
}]);
|
||||
setRestrictedDomainInput('');
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (restrictedDomainInput === '') return;
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
},
|
||||
]);
|
||||
setRestrictedDomainInput('');
|
||||
}}
|
||||
>
|
||||
{t('setting.system.email_restriction.buttons.fill')}
|
||||
</Button>
|
||||
}
|
||||
@ -392,14 +424,17 @@ const SystemSetting = () => {
|
||||
search
|
||||
selection
|
||||
allowAdditions
|
||||
value={EmailDomainWhitelist.map(item => item.value)}
|
||||
value={EmailDomainWhitelist.map((item) => item.value)}
|
||||
options={EmailDomainWhitelist}
|
||||
onAddItem={(e, { value }) => {
|
||||
setEmailDomainWhitelist([...EmailDomainWhitelist, {
|
||||
key: value,
|
||||
text: value,
|
||||
value: value
|
||||
}]);
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: value,
|
||||
text: value,
|
||||
value: value,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
onChange={(e, { value }) => {
|
||||
let newEmailDomainWhitelist = [];
|
||||
@ -407,7 +442,7 @@ const SystemSetting = () => {
|
||||
newEmailDomainWhitelist.push({
|
||||
key: item,
|
||||
text: item,
|
||||
value: item
|
||||
value: item,
|
||||
});
|
||||
});
|
||||
setEmailDomainWhitelist(newEmailDomainWhitelist);
|
||||
@ -476,7 +511,7 @@ const SystemSetting = () => {
|
||||
<Message>
|
||||
{t('setting.system.github.url_notice', {
|
||||
server_url: originInputs.ServerAddress,
|
||||
callback_url: `${originInputs.ServerAddress}/oauth/github`
|
||||
callback_url: `${originInputs.ServerAddress}/oauth/github`,
|
||||
})}
|
||||
</Message>
|
||||
<Form.Group widths={3}>
|
||||
@ -514,7 +549,7 @@ const SystemSetting = () => {
|
||||
<Message>
|
||||
{t('setting.system.lark.url_notice', {
|
||||
server_url: inputs.ServerAddress,
|
||||
callback_url: `${inputs.ServerAddress}/oauth/lark`
|
||||
callback_url: `${inputs.ServerAddress}/oauth/lark`,
|
||||
})}
|
||||
</Message>
|
||||
<Form.Group widths={3}>
|
||||
@ -560,7 +595,9 @@ const SystemSetting = () => {
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatServerAddress}
|
||||
placeholder={t('setting.system.wechat.server_address_placeholder')}
|
||||
placeholder={t(
|
||||
'setting.system.wechat.server_address_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.system.wechat.token')}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Header, Segment } from 'semantic-ui-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'semantic-ui-react';
|
||||
import { API, showError } from '../../helpers';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const About = () => {
|
||||
const { t } = useTranslation();
|
||||
const [about, setAbout] = useState('');
|
||||
const [aboutLoaded, setAboutLoaded] = useState(false);
|
||||
|
||||
@ -20,7 +22,7 @@ const About = () => {
|
||||
localStorage.setItem('about', aboutContent);
|
||||
} else {
|
||||
showError(message);
|
||||
setAbout('加载关于内容失败...');
|
||||
setAbout(t('about.loading_failed'));
|
||||
}
|
||||
setAboutLoaded(true);
|
||||
};
|
||||
@ -28,15 +30,16 @@ const About = () => {
|
||||
useEffect(() => {
|
||||
displayAbout().then();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{aboutLoaded && about === '' ? (
|
||||
<div className='dashboard-container'>
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header className='header'>关于系统</Card.Header>
|
||||
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
||||
项目仓库地址:
|
||||
<Card.Header className='header'>{t('about.title')}</Card.Header>
|
||||
<p>{t('about.description')}</p>
|
||||
{t('about.repository')}
|
||||
<a href='https://github.com/songquanpeng/one-api'>
|
||||
https://github.com/songquanpeng/one-api
|
||||
</a>
|
||||
|
Loading…
Reference in New Issue
Block a user