feat: i18n support

This commit is contained in:
JustSong 2025-02-02 00:12:22 +08:00
parent e7ea7c866f
commit d0965050a9
8 changed files with 447 additions and 170 deletions

View File

@ -434,7 +434,7 @@
"title": "System Settings",
"tabs": {
"personal": "Personal Settings",
"operation": "Operation Settings",
"operation": "Operation Settings",
"system": "System Settings",
"other": "Other Settings"
},
@ -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"
}
}

View File

@ -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 协议"
}
}

View File

@ -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>
)}

View File

@ -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 />
@ -264,19 +293,21 @@ const OperationSetting = () => {
/>
</Form.Group>
<Form.Group widths={4}>
<Form.Input
label={t('setting.operation.log.target_time')}
value={historyTimestamp}
<Form.Input
label={t('setting.operation.log.target_time')}
value={historyTimestamp}
type='datetime-local'
name='history_timestamp'
onChange={(e, { name, value }) => {
setHistoryTimestamp(value);
}}
}}
/>
</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>

View File

@ -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

View File

@ -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,26 +197,28 @@ 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>
{systemToken && (
<Form.Input
fluid
readOnly
value={systemToken}
<Form.Input
fluid
readOnly
value={systemToken}
onClick={handleSystemTokenClick}
style={{ marginTop: '10px' }}
/>
)}
{affLink && (
<Form.Input
fluid
readOnly
value={affLink}
<Form.Input
fluid
readOnly
value={affLink}
onClick={handleAffLinkClick}
style={{ marginTop: '10px' }}
/>
@ -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}>
{disableButton
? t('setting.personal.binding.email.get_code_retry', { countdown })
<Button
onClick={sendVerificationCode}
disabled={disableButton || loading}
>
{disableButton
? 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

View File

@ -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')}

View File

@ -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>