diff --git a/database/update-v4.2.3.1.sql b/database/update-v4.2.3.1.sql new file mode 100644 index 00000000..5029b160 --- /dev/null +++ b/database/update-v4.2.3.1.sql @@ -0,0 +1,3 @@ +INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES +(4, 'privacy', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"default_nickname\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"# 隐私政策\\n\\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。\",\"updated\":true}'), +(5, 'agreement', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"default_nickname\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"# 用户协议\\n\\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。\",\"updated\":true}'); diff --git a/database/update-v4.2.3.sql b/database/update-v4.2.3.sql index 0cdf9f14..98ac1d2f 100644 --- a/database/update-v4.2.3.sql +++ b/database/update-v4.2.3.sql @@ -2,4 +2,4 @@ ALTER TABLE `chatgpt_chat_models` ADD `category` VARCHAR(1024) NOT NULL DEFAULT ALTER TABLE `chatgpt_chat_models` ADD `description` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '模型类型描述' AFTER `id`; ALTER TABLE `chatgpt_orders` DROP `deleted_at`; ALTER TABLE `chatgpt_chat_history` DROP `deleted_at`; -ALTER TABLE `chatgpt_chat_items` DROP `deleted_at`; +ALTER TABLE `chatgpt_chat_items` DROP `deleted_at`; \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 82dd37b7..8e7ac086 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -28,6 +28,7 @@ "markdown-it": "^13.0.1", "markdown-it-emoji": "^2.0.0", "markdown-it-mathjax3": "^4.3.2", + "marked": "^15.0.11", "markmap-common": "^0.16.0", "markmap-lib": "^0.16.1", "markmap-toolbar": "^0.17.0", @@ -8721,6 +8722,17 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/marked": { + "version": "15.0.11", + "resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.11.tgz", + "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/markmap-common": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/markmap-common/-/markmap-common-0.16.0.tgz", diff --git a/web/package.json b/web/package.json index a7ea40e6..b6bdf84b 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,11 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@better-scroll/core": "^2.5.1", + "@better-scroll/mouse-wheel": "^2.5.1", + "@better-scroll/observe-dom": "^2.5.1", + "@better-scroll/pull-up": "^2.5.1", + "@better-scroll/scroll-bar": "^2.5.1", "@element-plus/icons-vue": "^2.3.1", "animate.css": "^4.1.1", "axios": "^0.27.2", @@ -23,6 +28,7 @@ "markdown-it": "^13.0.1", "markdown-it-emoji": "^2.0.0", "markdown-it-mathjax3": "^4.3.2", + "marked": "^15.0.11", "markmap-common": "^0.16.0", "markmap-lib": "^0.16.1", "markmap-toolbar": "^0.17.0", @@ -32,11 +38,6 @@ "pinia": "^2.1.4", "qrcode": "^1.5.3", "qs": "^6.11.1", - "@better-scroll/core": "^2.5.1", - "@better-scroll/mouse-wheel": "^2.5.1", - "@better-scroll/observe-dom": "^2.5.1", - "@better-scroll/pull-up": "^2.5.1", - "@better-scroll/scroll-bar": "^2.5.1", "sortablejs": "^1.15.0", "three": "^0.128.0", "vant": "^4.5.0", diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 07d2887e..7111f477 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -40,6 +40,16 @@ @keyup="handleKeyup" /> + +
+ + 我已阅读并同意 + 《用户协议》 + 和 + 《隐私政策》 + +
+
登录 { // 检查URL中是否存在token参数 @@ -110,6 +132,34 @@ onMounted(() => { title.value = 'Geek-AI' }) + // 获取用户协议 + httpGet('/api/config/get?key=agreement') + .then((res) => { + if (res.data && res.data.content) { + agreementContent.value = res.data.content + } else { + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。' + } + }) + .catch((e) => { + console.warn(e) + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。' + }) + + // 获取隐私政策 + httpGet('/api/config/get?key=privacy') + .then((res) => { + if (res.data && res.data.content) { + privacyContent.value = res.data.content + } else { + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。' + } + }) + .catch((e) => { + console.warn(e) + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。' + }) + getLicenseInfo() .then((res) => { licenseConfig.value = res.data @@ -141,6 +191,16 @@ const handleKeyup = (e) => { } const login = async function () { + if (!ruleForm.agreement) { + agreementError.value = true + isShaking.value = true + setTimeout(() => { + isShaking.value = false + }, 500) + showMessageError('请先阅读并同意用户协议') + return + } + await ruleFormRef.value.validate(async (valid) => { if (valid) { if (enableVerify.value) { @@ -170,8 +230,126 @@ const doLogin = (verifyData) => { showMessageError('登录失败,' + e.message) }) } + +const agreementError = ref(false) +const isShaking = ref(false) + +const handleAgreementChange = () => { + agreementError.value = !ruleForm.agreement + if (agreementError.value) { + isShaking.value = true + setTimeout(() => { + isShaking.value = false + }, 500) + } +} + +const openAgreement = () => { + // 使用弹窗显示用户协议内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(agreementContent.value)}
`, + '用户协议', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ) +} + +const openPrivacy = () => { + // 使用弹窗显示隐私政策内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(privacyContent.value)}
`, + '隐私政策', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ) +} + + diff --git a/web/src/views/Register.vue b/web/src/views/Register.vue index 864a8fdb..b6663206 100644 --- a/web/src/views/Register.vue +++ b/web/src/views/Register.vue @@ -62,6 +62,17 @@
+ +
+ + 我已阅读并同意 + 《用户协议》 + 和 + 《隐私政策》 + +
+
+ @@ -97,8 +108,9 @@ import AccountTop from "@/components/AccountTop.vue"; import AccountBg from "@/components/AccountBg.vue"; import { httpGet, httpPost } from "@/utils/http"; -import { ElMessage } from "element-plus"; +import { ElMessage, ElMessageBox } from "element-plus"; import { useRouter } from "vue-router"; +import MarkdownIt from "markdown-it"; import SendMsg from "@/components/SendMsg.vue"; import { arrayContains, isMobile } from "@/utils/libs"; @@ -119,6 +131,7 @@ const data = ref({ code: "", repass: "", invite_code: router.currentRoute.value.query["invite_code"], + agreement: false, }); const enableMobile = ref(false); @@ -130,6 +143,18 @@ const wxImg = ref("/images/wx.png"); const licenseConfig = ref({}); const enableVerify = ref(false); const captchaRef = ref(null); +const agreementError = ref(false); +const isShaking = ref(false); + +// 初始化markdown解析器 +const md = new MarkdownIt({ + html: true, + linkify: true, + typographer: true +}); + +const agreementContent = ref(''); +const privacyContent = ref(''); // 记录邀请码点击次数 if (data.value.invite_code) { @@ -168,6 +193,34 @@ getSystemInfo() ElMessage.error("获取系统配置失败:" + e.message); }); +// 获取用户协议 +httpGet('/api/config/get?key=agreement') + .then((res) => { + if (res.data && res.data.content) { + agreementContent.value = res.data.content; + } else { + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'; + } + }) + .catch((e) => { + console.warn(e); + agreementContent.value = '# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'; + }); + +// 获取隐私政策 +httpGet('/api/config/get?key=privacy') + .then((res) => { + if (res.data && res.data.content) { + privacyContent.value = res.data.content; + } else { + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'; + } + }) + .catch((e) => { + console.warn(e); + privacyContent.value = '# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'; + }); + getLicenseInfo() .then((res) => { licenseConfig.value = res.data; @@ -201,6 +254,16 @@ const submitRegister = () => { return showMessageError("请输入验证码"); } + if (!data.value.agreement) { + agreementError.value = true; + isShaking.value = true; + setTimeout(() => { + isShaking.value = false; + }, 500); + showMessageError("请先阅读并同意用户协议和隐私政策"); + return; + } + // 如果是用户名和密码登录,那么需要加载验证码 if (enableVerify.value && activeName.value === "username") { captchaRef.value.loadCaptcha(); @@ -228,6 +291,36 @@ const doSubmitRegister = (verifyData) => { showMessageError("注册失败," + e.message); }); }; + +const handleAgreementChange = () => { + agreementError.value = !data.value.agreement; +}; + +const openAgreement = () => { + // 使用弹窗显示用户协议内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(agreementContent.value)}
`, + '用户协议', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ); +}; + +const openPrivacy = () => { + // 使用弹窗显示隐私政策内容,支持Markdown格式 + ElMessageBox.alert( + `
${md.render(privacyContent.value)}
`, + '隐私政策', + { + confirmButtonText: '我已阅读', + dangerouslyUseHTMLString: true, + callback: () => {} + } + ); +}; + + diff --git a/web/src/views/admin/SysConfig.vue b/web/src/views/admin/SysConfig.vue index f3820f46..fddd2b99 100644 --- a/web/src/views/admin/SysConfig.vue +++ b/web/src/views/admin/SysConfig.vue @@ -311,6 +311,25 @@ + + + + +
+ 保存 +
+
+
+ + + + +
+ 保存 +
+
+
+ @@ -418,6 +437,8 @@ const loading = ref(true); const systemFormRef = ref(null); const models = ref([]); const notice = ref(""); +const agreement = ref(""); +const privacy = ref(""); const license = ref({ is_active: false }); const menus = ref([]); const mjModels = ref([ @@ -460,6 +481,24 @@ onMounted(() => { ElMessage.error("公告信息失败: " + e.message); }); + // 加载用户协议 + httpGet("/api/admin/config/get?key=agreement") + .then((res) => { + agreement.value = res.data["content"] || ''; + }) + .catch((e) => { + ElMessage.error("加载用户协议失败: " + e.message); + }); + + // 加载隐私政策 + httpGet("/api/admin/config/get?key=privacy") + .then((res) => { + privacy.value = res.data["content"] || ''; + }) + .catch((e) => { + ElMessage.error("加载隐私政策失败: " + e.message); + }); + httpGet("/api/admin/model/list") .then((res) => { models.value = res.data; @@ -517,6 +556,22 @@ const save = function (key) { .catch((e) => { ElMessage.error("操作失败:" + e.message); }); + } else if (key === "agreement") { + httpPost("/api/admin/config/update", { key: key, config: { content: agreement.value, updated: true } }) + .then(() => { + ElMessage.success("操作成功!"); + }) + .catch((e) => { + ElMessage.error("操作失败:" + e.message); + }); + } else if (key === "privacy") { + httpPost("/api/admin/config/update", { key: key, config: { content: privacy.value, updated: true } }) + .then(() => { + ElMessage.success("操作成功!"); + }) + .catch((e) => { + ElMessage.error("操作失败:" + e.message); + }); } };