feat(web): add privacy policy and user agreement settings & pages

Closes #1858
This commit is contained in:
キュビビイ
2025-10-02 23:03:58 +08:00
parent 2290acc86c
commit b0b6ab2ebc
13 changed files with 10203 additions and 3 deletions

View File

@@ -3,7 +3,20 @@ Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
published by th async function handleSubmit(e) {
if (password.length < 8) {
showError(t('密码长度不能少于 8 位!'));
return;
}
if (password !== password2) {
showError(t('两次输入的密码不一致'));
return;
}
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showError(t('请先阅读并同意用户协议和隐私政策'));
return;
}
if (username && password) {tware Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
@@ -82,6 +95,9 @@ const RegisterForm = () => {
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [hasUserAgreement, setHasUserAgreement] = useState(false);
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
const logo = getLogo();
const systemName = getSystemName();
@@ -106,6 +122,28 @@ const RegisterForm = () => {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
// 检查用户协议和隐私政策是否已设置
const checkTermsAvailability = async () => {
try {
const [userAgreementRes, privacyPolicyRes] = await Promise.all([
API.get('/api/user-agreement'),
API.get('/api/privacy-policy')
]);
if (userAgreementRes.data.success && userAgreementRes.data.data) {
setHasUserAgreement(true);
}
if (privacyPolicyRes.data.success && privacyPolicyRes.data.data) {
setHasPrivacyPolicy(true);
}
} catch (error) {
console.error('检查用户协议和隐私政策失败:', error);
}
};
checkTermsAvailability();
}, [status]);
useEffect(() => {
@@ -505,6 +543,44 @@ const RegisterForm = () => {
</>
)}
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='pt-4'>
<Form.Checkbox
checked={agreedToTerms}
onChange={(checked) => setAgreedToTerms(checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Form.Checkbox>
</div>
)}
<div className='space-y-2 pt-2'>
<Button
theme='solid'
@@ -513,6 +589,7 @@ const RegisterForm = () => {
htmlType='submit'
onClick={handleSubmit}
loading={registerLoading}
disabled={(hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms}
>
{t('注册')}
</Button>

View File

@@ -38,6 +38,8 @@ const OtherSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
Notice: '',
UserAgreement: '',
PrivacyPolicy: '',
SystemName: '',
Logo: '',
Footer: '',
@@ -69,6 +71,8 @@ const OtherSetting = () => {
const [loadingInput, setLoadingInput] = useState({
Notice: false,
UserAgreement: false,
PrivacyPolicy: false,
SystemName: false,
Logo: false,
HomePageContent: false,
@@ -96,6 +100,32 @@ const OtherSetting = () => {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
}
};
// 通用设置 - UserAgreement
const submitUserAgreement = async () => {
try {
setLoadingInput((loadingInput) => ({ ...loadingInput, UserAgreement: true }));
await updateOption('UserAgreement', inputs.UserAgreement);
showSuccess(t('用户协议已更新'));
} catch (error) {
console.error(t('用户协议更新失败'), error);
showError(t('用户协议更新失败'));
} finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, UserAgreement: false }));
}
};
// 通用设置 - PrivacyPolicy
const submitPrivacyPolicy = async () => {
try {
setLoadingInput((loadingInput) => ({ ...loadingInput, PrivacyPolicy: true }));
await updateOption('PrivacyPolicy', inputs.PrivacyPolicy);
showSuccess(t('隐私政策已更新'));
} catch (error) {
console.error(t('隐私政策更新失败'), error);
showError(t('隐私政策更新失败'));
} finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, PrivacyPolicy: false }));
}
};
// 个性化设置
const formAPIPersonalization = useRef();
// 个性化设置 - SystemName
@@ -324,6 +354,34 @@ const OtherSetting = () => {
<Button onClick={submitNotice} loading={loadingInput['Notice']}>
{t('设置公告')}
</Button>
<Form.TextArea
label={t('用户协议')}
placeholder={t(
'在此输入用户协议内容,支持 Markdown & HTML 代码',
)}
field={'UserAgreement'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议')}
/>
<Button onClick={submitUserAgreement} loading={loadingInput['UserAgreement']}>
{t('设置用户协议')}
</Button>
<Form.TextArea
label={t('隐私政策')}
placeholder={t(
'在此输入隐私政策内容,支持 Markdown & HTML 代码',
)}
field={'PrivacyPolicy'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策')}
/>
<Button onClick={submitPrivacyPolicy} loading={loadingInput['PrivacyPolicy']}>
{t('设置隐私政策')}
</Button>
</Form.Section>
</Card>
</Form>