diff --git a/.dockerignore b/.dockerignore index 0670cd7d1..781a7b550 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,5 @@ .gitignore Makefile docs -.eslintcache \ No newline at end of file +.eslintcache +.gocache \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3e6020e83..0cf4ce09a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ new-api .DS_Store tiktoken_cache .eslintcache +.gocache electron/node_modules electron/dist \ No newline at end of file diff --git a/controller/misc.go b/controller/misc.go index a3e017f87..8654848d5 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -43,6 +43,7 @@ func GetStatus(c *gin.Context) { defer common.OptionMapRWMutex.RUnlock() passkeySetting := system_setting.GetPasskeySettings() + legalSetting := system_setting.GetLegalSettings() data := gin.H{ "version": common.Version, @@ -108,6 +109,8 @@ func GetStatus(c *gin.Context) { "passkey_user_verification": passkeySetting.UserVerification, "passkey_attachment": passkeySetting.AttachmentPreference, "setup": constant.Setup, + "user_agreement_enabled": legalSetting.UserAgreement != "", + "privacy_policy_enabled": legalSetting.PrivacyPolicy != "", } // 根据启用状态注入可选内容 @@ -151,6 +154,24 @@ func GetAbout(c *gin.Context) { return } +func GetUserAgreement(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": system_setting.GetLegalSettings().UserAgreement, + }) + return +} + +func GetPrivacyPolicy(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": system_setting.GetLegalSettings().PrivacyPolicy, + }) + return +} + func GetMidjourney(c *gin.Context) { common.OptionMapRWMutex.RLock() defer common.OptionMapRWMutex.RUnlock() diff --git a/router/api-router.go b/router/api-router.go index 963abd105..7eefa936a 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -20,6 +20,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) apiRouter.GET("/notice", controller.GetNotice) + apiRouter.GET("/user-agreement", controller.GetUserAgreement) + apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy) apiRouter.GET("/about", controller.GetAbout) //apiRouter.GET("/midjourney", controller.GetMidjourney) apiRouter.GET("/home_page_content", controller.GetHomePageContent) diff --git a/setting/system_setting/legal.go b/setting/system_setting/legal.go new file mode 100644 index 000000000..5758ef71c --- /dev/null +++ b/setting/system_setting/legal.go @@ -0,0 +1,21 @@ +package system_setting + +import "one-api/setting/config" + +type LegalSettings struct { + UserAgreement string `json:"user_agreement"` + PrivacyPolicy string `json:"privacy_policy"` +} + +var defaultLegalSettings = LegalSettings{ + UserAgreement: "", + PrivacyPolicy: "", +} + +func init() { + config.GlobalConfig.Register("legal", &defaultLegalSettings) +} + +func GetLegalSettings() *LegalSettings { + return &defaultLegalSettings +} diff --git a/web/src/App.jsx b/web/src/App.jsx index 635742f91..06e364897 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -51,6 +51,8 @@ import SetupCheck from './components/layout/SetupCheck'; const Home = lazy(() => import('./pages/Home')); const Dashboard = lazy(() => import('./pages/Dashboard')); const About = lazy(() => import('./pages/About')); +const UserAgreement = lazy(() => import('./pages/UserAgreement')); +const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); function App() { const location = useLocation(); @@ -301,6 +303,22 @@ function App() { } /> + } key={location.pathname}> + + + } + /> + } key={location.pathname}> + + + } + /> { const [showTwoFA, setShowTwoFA] = useState(false); const [passkeySupported, setPasskeySupported] = useState(false); const [passkeyLoading, setPasskeyLoading] = useState(false); + const [agreedToTerms, setAgreedToTerms] = useState(false); + const [hasUserAgreement, setHasUserAgreement] = useState(false); + const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); const logo = getLogo(); const systemName = getSystemName(); @@ -103,6 +106,10 @@ const LoginForm = () => { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } + + // 从 status 获取用户协议和隐私政策的启用状态 + setHasUserAgreement(status.user_agreement_enabled || false); + setHasPrivacyPolicy(status.privacy_policy_enabled || false); }, [status]); useEffect(() => { @@ -118,6 +125,10 @@ const LoginForm = () => { }, []); const onWeChatLoginClicked = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setWechatLoading(true); setShowWeChatLoginModal(true); setWechatLoading(false); @@ -157,6 +168,10 @@ const LoginForm = () => { } async function handleSubmit(e) { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); return; @@ -208,6 +223,10 @@ const LoginForm = () => { // 添加Telegram登录处理函数 const onTelegramLoginClicked = async (response) => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } const fields = [ 'id', 'first_name', @@ -244,6 +263,10 @@ const LoginForm = () => { // 包装的GitHub登录点击处理 const handleGitHubClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setGithubLoading(true); try { onGitHubOAuthClicked(status.github_client_id); @@ -255,6 +278,10 @@ const LoginForm = () => { // 包装的OIDC登录点击处理 const handleOIDCClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setOidcLoading(true); try { onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id); @@ -266,6 +293,10 @@ const LoginForm = () => { // 包装的LinuxDO登录点击处理 const handleLinuxDOClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setLinuxdoLoading(true); try { onLinuxDOOAuthClicked(status.linuxdo_client_id); @@ -283,6 +314,10 @@ const LoginForm = () => { }; const handlePasskeyLogin = async () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } if (!passkeySupported) { showInfo('当前环境无法使用 Passkey 登录'); return; @@ -486,6 +521,44 @@ const LoginForm = () => { + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} + {!status.self_use_mode_enabled && (
@@ -554,6 +627,44 @@ const LoginForm = () => { prefix={} /> + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} +
diff --git a/web/src/components/auth/RegisterForm.jsx b/web/src/components/auth/RegisterForm.jsx index 9c98bdc3a..436a7b6b8 100644 --- a/web/src/components/auth/RegisterForm.jsx +++ b/web/src/components/auth/RegisterForm.jsx @@ -30,7 +30,7 @@ import { setUserData, } from '../../helpers'; import Turnstile from 'react-turnstile'; -import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui'; +import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import { @@ -82,6 +82,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 +109,10 @@ const RegisterForm = () => { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } + + // 从 status 获取用户协议和隐私政策的启用状态 + setHasUserAgreement(status.user_agreement_enabled || false); + setHasPrivacyPolicy(status.privacy_policy_enabled || false); }, [status]); useEffect(() => { @@ -505,6 +512,44 @@ const RegisterForm = () => { )} + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} +
diff --git a/web/src/components/common/DocumentRenderer/index.jsx b/web/src/components/common/DocumentRenderer/index.jsx new file mode 100644 index 000000000..383afc11d --- /dev/null +++ b/web/src/components/common/DocumentRenderer/index.jsx @@ -0,0 +1,243 @@ +/* +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 +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState } from 'react'; +import { API, showError } from '../../../helpers'; +import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui'; +const { Title } = Typography; +import { + IllustrationConstruction, + IllustrationConstructionDark, +} from '@douyinfe/semi-illustrations'; +import { useTranslation } from 'react-i18next'; +import MarkdownRenderer from '../markdown/MarkdownRenderer'; + +// 检查是否为 URL +const isUrl = (content) => { + try { + new URL(content.trim()); + return true; + } catch { + return false; + } +}; + +// 检查是否为 HTML 内容 +const isHtmlContent = (content) => { + if (!content || typeof content !== 'string') return false; + + // 检查是否包含HTML标签 + const htmlTagRegex = /<\/?[a-z][\s\S]*>/i; + return htmlTagRegex.test(content); +}; + +// 安全地渲染HTML内容 +const sanitizeHtml = (html) => { + // 创建一个临时元素来解析HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + // 提取样式 + const styles = Array.from(tempDiv.querySelectorAll('style')) + .map(style => style.innerHTML) + .join('\n'); + + // 提取body内容,如果没有body标签则使用全部内容 + const bodyContent = tempDiv.querySelector('body'); + const content = bodyContent ? bodyContent.innerHTML : html; + + return { content, styles }; +}; + +/** + * 通用文档渲染组件 + * @param {string} apiEndpoint - API 接口地址 + * @param {string} title - 文档标题 + * @param {string} cacheKey - 本地存储缓存键 + * @param {string} emptyMessage - 空内容时的提示消息 + */ +const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { + const { t } = useTranslation(); + const [content, setContent] = useState(''); + const [loading, setLoading] = useState(true); + const [htmlStyles, setHtmlStyles] = useState(''); + const [processedHtmlContent, setProcessedHtmlContent] = useState(''); + + const loadContent = async () => { + // 先从缓存中获取 + const cachedContent = localStorage.getItem(cacheKey) || ''; + if (cachedContent) { + setContent(cachedContent); + processContent(cachedContent); + setLoading(false); + } + + try { + const res = await API.get(apiEndpoint); + const { success, message, data } = res.data; + if (success && data) { + setContent(data); + processContent(data); + localStorage.setItem(cacheKey, data); + } else { + if (!cachedContent) { + showError(message || emptyMessage); + setContent(''); + } + } + } catch (error) { + if (!cachedContent) { + showError(emptyMessage); + setContent(''); + } + } finally { + setLoading(false); + } + }; + + const processContent = (rawContent) => { + if (isHtmlContent(rawContent)) { + const { content: htmlContent, styles } = sanitizeHtml(rawContent); + setProcessedHtmlContent(htmlContent); + setHtmlStyles(styles); + } else { + setProcessedHtmlContent(''); + setHtmlStyles(''); + } + }; + + useEffect(() => { + loadContent(); + }, []); + + // 处理HTML样式注入 + useEffect(() => { + const styleId = `document-renderer-styles-${cacheKey}`; + + if (htmlStyles) { + let styleEl = document.getElementById(styleId); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + styleEl.type = 'text/css'; + document.head.appendChild(styleEl); + } + styleEl.innerHTML = htmlStyles; + } else { + const el = document.getElementById(styleId); + if (el) el.remove(); + } + + return () => { + const el = document.getElementById(styleId); + if (el) el.remove(); + }; + }, [htmlStyles, cacheKey]); + + // 显示加载状态 + if (loading) { + return ( +
+ +
+ ); + } + + // 如果没有内容,显示空状态 + if (!content || content.trim() === '') { + return ( +
+ } + darkModeImage={} + className='p-8' + /> +
+ ); + } + + // 如果是 URL,显示链接卡片 + if (isUrl(content)) { + return ( +
+ +
+ {title} +

+ {t('管理员设置了外部链接,点击下方按钮访问')} +

+ + {t('访问' + title)} + +
+
+
+ ); + } + + // 如果是 HTML 内容,直接渲染 + if (isHtmlContent(content)) { + const { content: htmlContent, styles } = sanitizeHtml(content); + + // 设置样式(如果有的话) + useEffect(() => { + if (styles && styles !== htmlStyles) { + setHtmlStyles(styles); + } + }, [content, styles, htmlStyles]); + + return ( +
+
+
+ {title} +
+
+
+
+ ); + } + + // 其他内容统一使用 Markdown 渲染器 + return ( +
+
+
+ {title} +
+ +
+
+
+
+ ); +}; + +export default DocumentRenderer; \ No newline at end of file diff --git a/web/src/components/settings/OtherSetting.jsx b/web/src/components/settings/OtherSetting.jsx index 18119d242..646d21e74 100644 --- a/web/src/components/settings/OtherSetting.jsx +++ b/web/src/components/settings/OtherSetting.jsx @@ -34,10 +34,15 @@ import { useTranslation } from 'react-i18next'; import { StatusContext } from '../../context/Status'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; +const LEGAL_USER_AGREEMENT_KEY = 'legal.user_agreement'; +const LEGAL_PRIVACY_POLICY_KEY = 'legal.privacy_policy'; + const OtherSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ Notice: '', + [LEGAL_USER_AGREEMENT_KEY]: '', + [LEGAL_PRIVACY_POLICY_KEY]: '', SystemName: '', Logo: '', Footer: '', @@ -69,6 +74,8 @@ const OtherSetting = () => { const [loadingInput, setLoadingInput] = useState({ Notice: false, + [LEGAL_USER_AGREEMENT_KEY]: false, + [LEGAL_PRIVACY_POLICY_KEY]: false, SystemName: false, Logo: false, HomePageContent: false, @@ -96,6 +103,50 @@ const OtherSetting = () => { setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false })); } }; + // 通用设置 - UserAgreement + const submitUserAgreement = async () => { + try { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_USER_AGREEMENT_KEY]: true, + })); + await updateOption( + LEGAL_USER_AGREEMENT_KEY, + inputs[LEGAL_USER_AGREEMENT_KEY], + ); + showSuccess(t('用户协议已更新')); + } catch (error) { + console.error(t('用户协议更新失败'), error); + showError(t('用户协议更新失败')); + } finally { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_USER_AGREEMENT_KEY]: false, + })); + } + }; + // 通用设置 - PrivacyPolicy + const submitPrivacyPolicy = async () => { + try { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_PRIVACY_POLICY_KEY]: true, + })); + await updateOption( + LEGAL_PRIVACY_POLICY_KEY, + inputs[LEGAL_PRIVACY_POLICY_KEY], + ); + showSuccess(t('隐私政策已更新')); + } catch (error) { + console.error(t('隐私政策更新失败'), error); + showError(t('隐私政策更新失败')); + } finally { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_PRIVACY_POLICY_KEY]: false, + })); + } + }; // 个性化设置 const formAPIPersonalization = useRef(); // 个性化设置 - SystemName @@ -324,6 +375,40 @@ const OtherSetting = () => { + + + + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index d2d715ad9..9bd0ad2b1 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -245,6 +245,8 @@ "检查更新": "Check for updates", "公告": "Announcement", "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Enter user agreement content here, supports Markdown & HTML code", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code", "保存公告": "Save Announcement", "个性化设置": "Personalization Settings", "系统名称": "System Name", @@ -1261,6 +1263,8 @@ "仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour", "当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded", "设置公告": "Set notice", + "设置用户协议": "Set user agreement", + "设置隐私政策": "Set privacy policy", "设置 Logo": "Set Logo", "设置首页内容": "Set home page content", "设置关于": "Set about", @@ -2260,5 +2264,21 @@ "补单成功": "Order completed successfully", "补单失败": "Failed to complete order", "确认补单": "Confirm Order Completion", - "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?" + "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?", + "用户协议": "User Agreement", + "隐私政策": "Privacy Policy", + "用户协议更新失败": "Failed to update user agreement", + "隐私政策更新失败": "Failed to update privacy policy", + "管理员未设置用户协议内容": "Administrator has not set user agreement content", + "管理员未设置隐私政策内容": "Administrator has not set privacy policy content", + "加载用户协议内容失败...": "Failed to load user agreement content...", + "加载隐私政策内容失败...": "Failed to load privacy policy content...", + "我已阅读并同意": "I have read and agree to", + "和": " and ", + "请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement when registering", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy when registering", + "管理员设置了外部链接,点击下方按钮访问": "Administrator has set an external link, click the button below to access", + "访问用户协议": "Access User Agreement", + "访问隐私政策": "Access Privacy Policy" } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 9cb87b874..dcebda245 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2252,5 +2252,27 @@ "补单成功": "Commande complétée avec succès", "补单失败": "Échec de la complétion de la commande", "确认补单": "Confirmer la complétion", - "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?" + "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?", + "用户协议": "Accord utilisateur", + "隐私政策": "Politique de confidentialité", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de l'accord utilisateur, prend en charge le code Markdown et HTML", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de la politique de confidentialité, prend en charge le code Markdown et HTML", + "设置用户协议": "Définir l'accord utilisateur", + "设置隐私政策": "Définir la politique de confidentialité", + "用户协议已更新": "L'accord utilisateur a été mis à jour", + "隐私政策已更新": "La politique de confidentialité a été mise à jour", + "用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur", + "隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité", + "管理员未设置用户协议内容": "L'administrateur n'a pas défini le contenu de l'accord utilisateur", + "管理员未设置隐私政策内容": "L'administrateur n'a pas défini le contenu de la politique de confidentialité", + "加载用户协议内容失败...": "Échec du chargement du contenu de l'accord utilisateur...", + "加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...", + "我已阅读并同意": "J'ai lu et j'accepte", + "和": " et ", + "请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher qu'ils ont lu l'accord utilisateur lors de l'inscription", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher qu'ils ont lu la politique de confidentialité lors de l'inscription", + "管理员设置了外部链接,点击下方按钮访问": "L'administrateur a défini un lien externe, cliquez sur le bouton ci-dessous pour accéder", + "访问用户协议": "Accéder à l'accord utilisateur", + "访问隐私政策": "Accéder à la politique de confidentialité" } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index dcb693ecd..8db3e215f 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -111,5 +111,27 @@ "补单失败": "补单失败", "确认补单": "确认补单", "是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?", - "操作": "操作" + "操作": "操作", + "用户协议": "用户协议", + "隐私政策": "隐私政策", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "在此输入用户协议内容,支持 Markdown & HTML 代码", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码", + "设置用户协议": "设置用户协议", + "设置隐私政策": "设置隐私政策", + "用户协议已更新": "用户协议已更新", + "隐私政策已更新": "隐私政策已更新", + "用户协议更新失败": "用户协议更新失败", + "隐私政策更新失败": "隐私政策更新失败", + "管理员未设置用户协议内容": "管理员未设置用户协议内容", + "管理员未设置隐私政策内容": "管理员未设置隐私政策内容", + "加载用户协议内容失败...": "加载用户协议内容失败...", + "加载隐私政策内容失败...": "加载隐私政策内容失败...", + "我已阅读并同意": "我已阅读并同意", + "和": "和", + "请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策", + "管理员设置了外部链接,点击下方按钮访问": "管理员设置了外部链接,点击下方按钮访问", + "访问用户协议": "访问用户协议", + "访问隐私政策": "访问隐私政策" } diff --git a/web/src/pages/PrivacyPolicy/index.jsx b/web/src/pages/PrivacyPolicy/index.jsx new file mode 100644 index 000000000..026290b18 --- /dev/null +++ b/web/src/pages/PrivacyPolicy/index.jsx @@ -0,0 +1,37 @@ +/* +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 +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import DocumentRenderer from '../../components/common/DocumentRenderer'; + +const PrivacyPolicy = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default PrivacyPolicy; \ No newline at end of file diff --git a/web/src/pages/UserAgreement/index.jsx b/web/src/pages/UserAgreement/index.jsx new file mode 100644 index 000000000..965a0dd54 --- /dev/null +++ b/web/src/pages/UserAgreement/index.jsx @@ -0,0 +1,37 @@ +/* +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 +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import DocumentRenderer from '../../components/common/DocumentRenderer'; + +const UserAgreement = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default UserAgreement; \ No newline at end of file