Merge branch 'main-upstream' into pr/custom-currency-1923
# Conflicts: # web/src/components/settings/personal/cards/AccountManagement.jsx # web/src/components/table/channels/modals/EditChannelModal.jsx # web/src/hooks/channels/useChannelsData.jsx # web/src/hooks/common/useSidebar.js # web/src/i18n/locales/fr.json # web/src/pages/Setting/Operation/SettingsGeneral.jsx
This commit is contained in:
@@ -26,6 +26,10 @@ import {
|
||||
showInfo,
|
||||
showSuccess,
|
||||
setStatusData,
|
||||
prepareCredentialCreationOptions,
|
||||
buildRegistrationResult,
|
||||
isPasskeySupported,
|
||||
setUserData,
|
||||
} from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
@@ -66,6 +70,10 @@ const PersonalSetting = () => {
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [systemToken, setSystemToken] = useState('');
|
||||
const [passkeyStatus, setPasskeyStatus] = useState({ enabled: false });
|
||||
const [passkeyRegisterLoading, setPasskeyRegisterLoading] = useState(false);
|
||||
const [passkeyDeleteLoading, setPasskeyDeleteLoading] = useState(false);
|
||||
const [passkeySupported, setPasskeySupported] = useState(false);
|
||||
const [notificationSettings, setNotificationSettings] = useState({
|
||||
warningType: 'email',
|
||||
warningThreshold: 100000,
|
||||
@@ -73,6 +81,9 @@ const PersonalSetting = () => {
|
||||
webhookSecret: '',
|
||||
notificationEmail: '',
|
||||
barkUrl: '',
|
||||
gotifyUrl: '',
|
||||
gotifyToken: '',
|
||||
gotifyPriority: 5,
|
||||
acceptUnsetModelRatioModel: false,
|
||||
recordIpLog: false,
|
||||
});
|
||||
@@ -112,6 +123,10 @@ const PersonalSetting = () => {
|
||||
})();
|
||||
|
||||
getUserData();
|
||||
|
||||
isPasskeySupported()
|
||||
.then(setPasskeySupported)
|
||||
.catch(() => setPasskeySupported(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -137,6 +152,12 @@ const PersonalSetting = () => {
|
||||
webhookSecret: settings.webhook_secret || '',
|
||||
notificationEmail: settings.notification_email || '',
|
||||
barkUrl: settings.bark_url || '',
|
||||
gotifyUrl: settings.gotify_url || '',
|
||||
gotifyToken: settings.gotify_token || '',
|
||||
gotifyPriority:
|
||||
settings.gotify_priority !== undefined
|
||||
? settings.gotify_priority
|
||||
: 5,
|
||||
acceptUnsetModelRatioModel:
|
||||
settings.accept_unset_model_ratio_model || false,
|
||||
recordIpLog: settings.record_ip_log || false,
|
||||
@@ -160,11 +181,90 @@ const PersonalSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPasskeyStatus = async () => {
|
||||
try {
|
||||
const res = await API.get('/api/user/passkey');
|
||||
const { success, data, message } = res.data;
|
||||
if (success) {
|
||||
setPasskeyStatus({
|
||||
enabled: data?.enabled || false,
|
||||
last_used_at: data?.last_used_at || null,
|
||||
backup_eligible: data?.backup_eligible || false,
|
||||
backup_state: data?.backup_state || false,
|
||||
});
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误,保留默认状态
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterPasskey = async () => {
|
||||
if (!passkeySupported || !window.PublicKeyCredential) {
|
||||
showInfo(t('当前设备不支持 Passkey'));
|
||||
return;
|
||||
}
|
||||
setPasskeyRegisterLoading(true);
|
||||
try {
|
||||
const beginRes = await API.post('/api/user/passkey/register/begin');
|
||||
const { success, message, data } = beginRes.data;
|
||||
if (!success) {
|
||||
showError(message || t('无法发起 Passkey 注册'));
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data);
|
||||
const credential = await navigator.credentials.create({ publicKey });
|
||||
const payload = buildRegistrationResult(credential);
|
||||
if (!payload) {
|
||||
showError(t('Passkey 注册失败,请重试'));
|
||||
return;
|
||||
}
|
||||
|
||||
const finishRes = await API.post('/api/user/passkey/register/finish', payload);
|
||||
if (finishRes.data.success) {
|
||||
showSuccess(t('Passkey 注册成功'));
|
||||
await loadPasskeyStatus();
|
||||
} else {
|
||||
showError(finishRes.data.message || t('Passkey 注册失败,请重试'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
showInfo(t('已取消 Passkey 注册'));
|
||||
} else {
|
||||
showError(t('Passkey 注册失败,请重试'));
|
||||
}
|
||||
} finally {
|
||||
setPasskeyRegisterLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePasskey = async () => {
|
||||
setPasskeyDeleteLoading(true);
|
||||
try {
|
||||
const res = await API.delete('/api/user/passkey');
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('Passkey 已解绑'));
|
||||
await loadPasskeyStatus();
|
||||
} else {
|
||||
showError(message || t('操作失败,请重试'));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('操作失败,请重试'));
|
||||
} finally {
|
||||
setPasskeyDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
setUserData(data);
|
||||
await loadPasskeyStatus();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -315,6 +415,12 @@ const PersonalSetting = () => {
|
||||
webhook_secret: notificationSettings.webhookSecret,
|
||||
notification_email: notificationSettings.notificationEmail,
|
||||
bark_url: notificationSettings.barkUrl,
|
||||
gotify_url: notificationSettings.gotifyUrl,
|
||||
gotify_token: notificationSettings.gotifyToken,
|
||||
gotify_priority: (() => {
|
||||
const parsed = parseInt(notificationSettings.gotifyPriority);
|
||||
return isNaN(parsed) ? 5 : parsed;
|
||||
})(),
|
||||
accept_unset_model_ratio_model:
|
||||
notificationSettings.acceptUnsetModelRatioModel,
|
||||
record_ip_log: notificationSettings.recordIpLog,
|
||||
@@ -352,6 +458,12 @@ const PersonalSetting = () => {
|
||||
handleSystemTokenClick={handleSystemTokenClick}
|
||||
setShowChangePasswordModal={setShowChangePasswordModal}
|
||||
setShowAccountDeleteModal={setShowAccountDeleteModal}
|
||||
passkeyStatus={passkeyStatus}
|
||||
passkeySupported={passkeySupported}
|
||||
passkeyRegisterLoading={passkeyRegisterLoading}
|
||||
passkeyDeleteLoading={passkeyDeleteLoading}
|
||||
onPasskeyRegister={handleRegisterPasskey}
|
||||
onPasskeyDelete={handleRemovePasskey}
|
||||
/>
|
||||
|
||||
{/* 右侧:其他设置 */}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Spin,
|
||||
Card,
|
||||
Radio,
|
||||
Select,
|
||||
} from '@douyinfe/semi-ui';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
@@ -76,6 +77,13 @@ const SystemSetting = () => {
|
||||
TurnstileSiteKey: '',
|
||||
TurnstileSecretKey: '',
|
||||
RegisterEnabled: '',
|
||||
'passkey.enabled': '',
|
||||
'passkey.rp_display_name': '',
|
||||
'passkey.rp_id': '',
|
||||
'passkey.origins': [],
|
||||
'passkey.allow_insecure_origin': '',
|
||||
'passkey.user_verification': 'preferred',
|
||||
'passkey.attachment_preference': '',
|
||||
EmailDomainRestrictionEnabled: '',
|
||||
EmailAliasRestrictionEnabled: '',
|
||||
SMTPSSLEnabled: '',
|
||||
@@ -172,9 +180,25 @@ const SystemSetting = () => {
|
||||
case 'SMTPSSLEnabled':
|
||||
case 'LinuxDOOAuthEnabled':
|
||||
case 'oidc.enabled':
|
||||
case 'passkey.enabled':
|
||||
case 'passkey.allow_insecure_origin':
|
||||
case 'WorkerAllowHttpImageRequestEnabled':
|
||||
item.value = toBoolean(item.value);
|
||||
break;
|
||||
case 'passkey.origins':
|
||||
// origins是逗号分隔的字符串,直接使用
|
||||
item.value = item.value || '';
|
||||
break;
|
||||
case 'passkey.rp_display_name':
|
||||
case 'passkey.rp_id':
|
||||
case 'passkey.attachment_preference':
|
||||
// 确保字符串字段不为null/undefined
|
||||
item.value = item.value || '';
|
||||
break;
|
||||
case 'passkey.user_verification':
|
||||
// 确保有默认值
|
||||
item.value = item.value || 'preferred';
|
||||
break;
|
||||
case 'Price':
|
||||
case 'MinTopUp':
|
||||
item.value = parseFloat(item.value);
|
||||
@@ -583,6 +607,36 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitPasskeySettings = async () => {
|
||||
// 使用formApi直接获取当前表单值
|
||||
const formValues = formApiRef.current?.getValues() || {};
|
||||
|
||||
const options = [];
|
||||
|
||||
options.push({
|
||||
key: 'passkey.rp_display_name',
|
||||
value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.rp_id',
|
||||
value: formValues['passkey.rp_id'] || inputs['passkey.rp_id'] || '',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.user_verification',
|
||||
value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.attachment_preference',
|
||||
value: formValues['passkey.attachment_preference'] || inputs['passkey.attachment_preference'] || '',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.origins',
|
||||
value: formValues['passkey.origins'] || inputs['passkey.origins'] || '',
|
||||
});
|
||||
|
||||
await updateOptions(options);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = async (optionKey, event) => {
|
||||
const value = event.target.checked;
|
||||
|
||||
@@ -985,6 +1039,116 @@ const SystemSetting = () => {
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Form.Section text={t('配置 Passkey')}>
|
||||
<Text>{t('用以支持基于 WebAuthn 的无密码登录注册')}</Text>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t('Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式')}
|
||||
style={{ marginBottom: 20, marginTop: 16 }}
|
||||
/>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||
<Form.Checkbox
|
||||
field="['passkey.enabled']"
|
||||
noLabel
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange('passkey.enabled', e)
|
||||
}
|
||||
>
|
||||
{t('允许通过 Passkey 登录 & 认证')}
|
||||
</Form.Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field="['passkey.rp_display_name']"
|
||||
label={t('服务显示名称')}
|
||||
placeholder={t('默认使用系统名称')}
|
||||
extraText={t('用户注册时看到的网站名称,比如\'我的网站\'')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field="['passkey.rp_id']"
|
||||
label={t('网站域名标识')}
|
||||
placeholder={t('例如:example.com')}
|
||||
extraText={t('留空则默认使用服务器地址,注意不能携带http://或者https://')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Select
|
||||
field="['passkey.user_verification']"
|
||||
label={t('安全验证级别')}
|
||||
placeholder={t('是否要求指纹/面容等生物识别')}
|
||||
optionList={[
|
||||
{ label: t('推荐使用(用户可选)'), value: 'preferred' },
|
||||
{ label: t('强制要求'), value: 'required' },
|
||||
{ label: t('不建议使用'), value: 'discouraged' },
|
||||
]}
|
||||
extraText={t('推荐:用户可以选择是否使用指纹等验证')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Select
|
||||
field="['passkey.attachment_preference']"
|
||||
label={t('设备类型偏好')}
|
||||
placeholder={t('选择支持的认证设备类型')}
|
||||
optionList={[
|
||||
{ label: t('不限制'), value: '' },
|
||||
{ label: t('本设备内置'), value: 'platform' },
|
||||
{ label: t('外接设备'), value: 'cross-platform' },
|
||||
]}
|
||||
extraText={t('本设备:手机指纹/面容,外接:USB安全密钥')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||
<Form.Checkbox
|
||||
field="['passkey.allow_insecure_origin']"
|
||||
noLabel
|
||||
extraText={t('仅用于开发环境,生产环境应使用 HTTPS')}
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange('passkey.allow_insecure_origin', e)
|
||||
}
|
||||
>
|
||||
{t('允许不安全的 Origin(HTTP)')}
|
||||
</Form.Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||
<Form.Input
|
||||
field="['passkey.origins']"
|
||||
label={t('允许的 Origins')}
|
||||
placeholder={t('填写带https的域名,逗号分隔')}
|
||||
extraText={t('为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={submitPasskeySettings} style={{ marginTop: 16 }}>
|
||||
{t('保存 Passkey 设置')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Form.Section text={t('配置邮箱域名白名单')}>
|
||||
<Text>{t('用以防止恶意用户利用临时邮箱批量注册')}</Text>
|
||||
|
||||
@@ -59,6 +59,12 @@ const AccountManagement = ({
|
||||
handleSystemTokenClick,
|
||||
setShowChangePasswordModal,
|
||||
setShowAccountDeleteModal,
|
||||
passkeyStatus,
|
||||
passkeySupported,
|
||||
passkeyRegisterLoading,
|
||||
passkeyDeleteLoading,
|
||||
onPasskeyRegister,
|
||||
onPasskeyDelete,
|
||||
}) => {
|
||||
const renderAccountInfo = (accountId, label) => {
|
||||
if (!accountId || accountId === '') {
|
||||
@@ -87,6 +93,10 @@ const AccountManagement = ({
|
||||
const isBound = (accountId) => Boolean(accountId);
|
||||
const [showTelegramBindModal, setShowTelegramBindModal] =
|
||||
React.useState(false);
|
||||
const passkeyEnabled = passkeyStatus?.enabled;
|
||||
const lastUsedLabel = passkeyStatus?.last_used_at
|
||||
? new Date(passkeyStatus.last_used_at).toLocaleString()
|
||||
: t('尚未使用');
|
||||
|
||||
return (
|
||||
<Card className='!rounded-2xl'>
|
||||
@@ -479,6 +489,71 @@ const AccountManagement = ({
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Passkey 设置 */}
|
||||
<Card className='!rounded-xl w-full'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
|
||||
<IconKey size='large' className='text-slate-600' />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className='mb-1'>
|
||||
{t('Passkey 登录')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{passkeyEnabled
|
||||
? t('已启用 Passkey,无需密码即可登录')
|
||||
: t('使用 Passkey 实现免密且更安全的登录体验')}
|
||||
</Typography.Text>
|
||||
<div className='mt-2 text-xs text-gray-500 space-y-1'>
|
||||
<div>
|
||||
{t('最后使用时间')}:{lastUsedLabel}
|
||||
</div>
|
||||
{/*{passkeyEnabled && (*/}
|
||||
{/* <div>*/}
|
||||
{/* {t('备份支持')}:*/}
|
||||
{/* {passkeyStatus?.backup_eligible*/}
|
||||
{/* ? t('支持备份')*/}
|
||||
{/* : t('不支持')}*/}
|
||||
{/* ,{t('备份状态')}:*/}
|
||||
{/* {passkeyStatus?.backup_state ? t('已备份') : t('未备份')}*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
{!passkeySupported && (
|
||||
<div className='text-amber-600'>
|
||||
{t('当前设备不支持 Passkey')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type={passkeyEnabled ? 'danger' : 'primary'}
|
||||
theme={passkeyEnabled ? 'solid' : 'solid'}
|
||||
onClick={
|
||||
passkeyEnabled
|
||||
? () => {
|
||||
Modal.confirm({
|
||||
title: t('确认解绑 Passkey'),
|
||||
content: t('解绑后将无法使用 Passkey 登录,确定要继续吗?'),
|
||||
okText: t('确认解绑'),
|
||||
cancelText: t('取消'),
|
||||
okType: 'danger',
|
||||
onOk: onPasskeyDelete,
|
||||
});
|
||||
}
|
||||
: onPasskeyRegister
|
||||
}
|
||||
className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`}
|
||||
icon={<IconKey />}
|
||||
disabled={!passkeySupported && !passkeyEnabled}
|
||||
loading={passkeyEnabled ? passkeyDeleteLoading : passkeyRegisterLoading}
|
||||
>
|
||||
{passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 两步验证设置 */}
|
||||
<TwoFASetting t={t} />
|
||||
|
||||
|
||||
@@ -400,6 +400,7 @@ const NotificationSettings = ({
|
||||
<Radio value='email'>{t('邮件通知')}</Radio>
|
||||
<Radio value='webhook'>{t('Webhook通知')}</Radio>
|
||||
<Radio value='bark'>{t('Bark通知')}</Radio>
|
||||
<Radio value='gotify'>{t('Gotify通知')}</Radio>
|
||||
</Form.RadioGroup>
|
||||
|
||||
<Form.AutoComplete
|
||||
@@ -589,7 +590,108 @@ const NotificationSettings = ({
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 font-medium'
|
||||
>
|
||||
Bark 官方文档
|
||||
Bark {t('官方文档')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Gotify推送设置 */}
|
||||
{notificationSettings.warningType === 'gotify' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field='gotifyUrl'
|
||||
label={t('Gotify服务器地址')}
|
||||
placeholder={t(
|
||||
'请输入Gotify服务器地址,例如: https://gotify.example.com',
|
||||
)}
|
||||
onChange={(val) => handleFormChange('gotifyUrl', val)}
|
||||
prefix={<IconLink />}
|
||||
extraText={t(
|
||||
'支持HTTP和HTTPS,填写Gotify服务器的完整URL地址',
|
||||
)}
|
||||
showClear
|
||||
rules={[
|
||||
{
|
||||
required:
|
||||
notificationSettings.warningType === 'gotify',
|
||||
message: t('请输入Gotify服务器地址'),
|
||||
},
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: t('Gotify服务器地址必须以http://或https://开头'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field='gotifyToken'
|
||||
label={t('Gotify应用令牌')}
|
||||
placeholder={t('请输入Gotify应用令牌')}
|
||||
onChange={(val) => handleFormChange('gotifyToken', val)}
|
||||
prefix={<IconKey />}
|
||||
extraText={t(
|
||||
'在Gotify服务器创建应用后获得的令牌,用于发送通知',
|
||||
)}
|
||||
showClear
|
||||
rules={[
|
||||
{
|
||||
required:
|
||||
notificationSettings.warningType === 'gotify',
|
||||
message: t('请输入Gotify应用令牌'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Form.AutoComplete
|
||||
field='gotifyPriority'
|
||||
label={t('消息优先级')}
|
||||
placeholder={t('请选择消息优先级')}
|
||||
data={[
|
||||
{ value: 0, label: t('0 - 最低') },
|
||||
{ value: 2, label: t('2 - 低') },
|
||||
{ value: 5, label: t('5 - 正常(默认)') },
|
||||
{ value: 8, label: t('8 - 高') },
|
||||
{ value: 10, label: t('10 - 最高') },
|
||||
]}
|
||||
onChange={(val) =>
|
||||
handleFormChange('gotifyPriority', val)
|
||||
}
|
||||
prefix={<IconBell />}
|
||||
extraText={t('消息优先级,范围0-10,默认为5')}
|
||||
style={{ width: '100%', maxWidth: '300px' }}
|
||||
/>
|
||||
|
||||
<div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
|
||||
<div className='text-sm text-gray-700 mb-3'>
|
||||
<strong>{t('配置说明')}</strong>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 space-y-2'>
|
||||
<div>
|
||||
1. {t('在Gotify服务器的应用管理中创建新应用')}
|
||||
</div>
|
||||
<div>
|
||||
2.{' '}
|
||||
{t(
|
||||
'复制应用的令牌(Token)并填写到上方的应用令牌字段',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
3. {t('填写Gotify服务器的完整URL地址')}
|
||||
</div>
|
||||
<div className='mt-3 pt-3 border-t border-gray-200'>
|
||||
<span className='text-gray-400'>
|
||||
{t('更多信息请参考')}
|
||||
</span>{' '}
|
||||
<a
|
||||
href='https://gotify.net/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 font-medium'
|
||||
>
|
||||
Gotify {t('官方文档')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user