diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index d94c475b..cbe3f6ae 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -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" } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 176ccb43..bcbbfc68 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -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 协议" } } diff --git a/web/default/src/components/Footer.js b/web/default/src/components/Footer.js index edeeaa50..b222a999 100644 --- a/web/default/src/components/Footer.js +++ b/web/default/src/components/Footer.js @@ -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 = () => { {systemName} {process.env.REACT_APP_VERSION}{' '} - 由{' '} + {t('footer.built_by')}{' '} - JustSong + {t('footer.built_by_name')} {' '} - 构建,源代码遵循{' '} + {t('footer.license')}{' '} - MIT 协议 + {t('footer.mit')} )} diff --git a/web/default/src/components/OperationSetting.js b/web/default/src/components/OperationSetting.js index f4c87e7b..fcefd383 100644 --- a/web/default/src/components/OperationSetting.js +++ b/web/default/src/components/OperationSetting.js @@ -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 = () => { - - {t('setting.operation.quota.title')} - + {t('setting.operation.quota.title')} { value={inputs.QuotaForInviter} type='number' min='0' - placeholder={t('setting.operation.quota.inviter_reward_placeholder')} + placeholder={t( + 'setting.operation.quota.inviter_reward_placeholder' + )} /> { value={inputs.QuotaForInvitee} type='number' min='0' - placeholder={t('setting.operation.quota.invitee_reward_placeholder')} + placeholder={t( + 'setting.operation.quota.invitee_reward_placeholder' + )} /> - { - submitConfig('quota').then(); - }}> + { + submitConfig('quota').then(); + }} + > {t('setting.operation.quota.buttons.save')} - - {t('setting.operation.ratio.title')} - + {t('setting.operation.ratio.title')} { placeholder={t('setting.operation.ratio.group.placeholder')} /> - { - submitConfig('ratio').then(); - }}> + { + submitConfig('ratio').then(); + }} + > {t('setting.operation.ratio.buttons.save')} @@ -264,19 +293,21 @@ const OperationSetting = () => { /> - { setHistoryTimestamp(value); - }} + }} /> - { - deleteHistoryLogs().then(); - }}> + { + deleteHistoryLogs().then(); + }} + > {t('setting.operation.log.buttons.clean')} @@ -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' + )} /> { value={inputs.QuotaRemindThreshold} type='number' min='0' - placeholder={t('setting.operation.monitor.quota_reminder_placeholder')} + placeholder={t( + 'setting.operation.monitor.quota_reminder_placeholder' + )} /> @@ -318,9 +353,11 @@ const OperationSetting = () => { onChange={handleInputChange} /> - { - submitConfig('monitor').then(); - }}> + { + submitConfig('monitor').then(); + }} + > {t('setting.operation.monitor.buttons.save')} @@ -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' + )} /> { 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' + )} /> { onChange={handleInputChange} autoComplete='new-password' value={inputs.RetryTimes} - placeholder={t('setting.operation.general.retry_times_placeholder')} + placeholder={t( + 'setting.operation.general.retry_times_placeholder' + )} /> @@ -387,9 +430,11 @@ const OperationSetting = () => { onChange={handleInputChange} /> - { - submitConfig('general').then(); - }}> + { + submitConfig('general').then(); + }} + > {t('setting.operation.general.buttons.save')} diff --git a/web/default/src/components/OtherSetting.js b/web/default/src/components/OtherSetting.js index ae924d9f..a45bdeb4 100644 --- a/web/default/src/components/OtherSetting.js +++ b/web/default/src/components/OtherSetting.js @@ -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 = () => { - 通用设置 - 检查更新 + {t('setting.other.notice.title')} - 保存公告 + + {t('setting.other.notice.buttons.save')} + + - 个性化设置 + {t('setting.other.system.title')} - 设置系统名称 + + {t('setting.other.system.buttons.save_name')} + 主题名称(当前可用主题)} - placeholder='请输入主题名称' + label={ + + {t('setting.other.system.theme.title')}( + + {t('setting.other.system.theme.link')} + + ) + + } + placeholder={t('setting.other.system.theme.placeholder')} value={inputs.Theme} name='Theme' onChange={handleInputChange} /> - 设置主题(重启生效) + + {t('setting.other.system.buttons.save_theme')} + - 设置 Logo + + {t('setting.other.system.buttons.save_logo')} + + + + {t('setting.other.content.title')} - submitOption('HomePageContent')}>保存首页内容 + submitOption('HomePageContent')}> + {t('setting.other.content.buttons.save_homepage')} + - 保存关于 - 移除 One API - 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。 + + {t('setting.other.content.buttons.save_about')} + + {t('setting.other.copyright.notice')} - 设置页脚 + submitOption('Footer')}> + {t('setting.other.content.buttons.save_footer')} + { 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 ( {t('setting.personal.general.title')} - - {t('setting.personal.general.system_token_notice')} - + {t('setting.personal.general.system_token_notice')} {t('setting.personal.general.buttons.update_profile')} @@ -184,26 +197,28 @@ const PersonalSetting = () => { {t('setting.personal.general.buttons.copy_invite')} - { - setShowAccountDeleteModal(true); - }}> + { + setShowAccountDeleteModal(true); + }} + > {t('setting.personal.general.buttons.delete_account')} - + {systemToken && ( - )} {affLink && ( - @@ -230,7 +245,9 @@ const PersonalSetting = () => { { - {disableButton - ? t('setting.personal.binding.email.get_code_retry', { countdown }) + + {disableButton + ? t('setting.personal.binding.email.get_code_retry', { + countdown, + }) : t('setting.personal.binding.email.get_code')} } /> { }} /> )} - + { size={'tiny'} style={{ maxWidth: '450px' }} > - {t('setting.personal.delete_account.title')} + + {t('setting.personal.delete_account.title')} + {t('setting.personal.delete_account.warning')} { }} /> )} - + { @@ -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 ( @@ -266,7 +287,9 @@ const SystemSetting = () => { { size={'tiny'} style={{ maxWidth: '450px' }} > - {t('setting.system.password_login.warning.title')} + + {t('setting.system.password_login.warning.title')} + {t('setting.system.password_login.warning.content')} @@ -364,21 +389,28 @@ const SystemSetting = () => { { setRestrictedDomainInput(value); }} action={ - { - if (restrictedDomainInput === '') return; - setEmailDomainWhitelist([...EmailDomainWhitelist, { - key: restrictedDomainInput, - text: restrictedDomainInput, - value: restrictedDomainInput - }]); - setRestrictedDomainInput(''); - }}> + { + if (restrictedDomainInput === '') return; + setEmailDomainWhitelist([ + ...EmailDomainWhitelist, + { + key: restrictedDomainInput, + text: restrictedDomainInput, + value: restrictedDomainInput, + }, + ]); + setRestrictedDomainInput(''); + }} + > {t('setting.system.email_restriction.buttons.fill')} } @@ -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 = () => { {t('setting.system.github.url_notice', { server_url: originInputs.ServerAddress, - callback_url: `${originInputs.ServerAddress}/oauth/github` + callback_url: `${originInputs.ServerAddress}/oauth/github`, })} @@ -514,7 +549,7 @@ const SystemSetting = () => { {t('setting.system.lark.url_notice', { server_url: inputs.ServerAddress, - callback_url: `${inputs.ServerAddress}/oauth/lark` + callback_url: `${inputs.ServerAddress}/oauth/lark`, })} @@ -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' + )} /> { + 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 === '' ? ( - 关于系统 - 可在设置页面设置关于内容,支持 HTML & Markdown - 项目仓库地址: + {t('about.title')} + {t('about.description')} + {t('about.repository')} https://github.com/songquanpeng/one-api
{t('setting.system.password_login.warning.content')}
可在设置页面设置关于内容,支持 HTML & Markdown
{t('about.description')}