用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。

This commit is contained in:
清柯
2025-05-08 03:19:51 +08:00
parent 26c18fcd5a
commit c4fe6c825e
7 changed files with 429 additions and 8 deletions

View File

@@ -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}');

View File

@@ -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`;

12
web/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -40,6 +40,16 @@
@keyup="handleKeyup"
/>
</el-form-item>
<el-form-item label="" prop="agreement" :class="{'agreement-error': agreementError}">
<div class="agreement-box" :class="{'shake': isShaking}">
<el-checkbox v-model="ruleForm.agreement" @change="handleAgreementChange">
我已阅读并同意
<span class="agreement-link" @click.stop.prevent="openAgreement">用户协议</span>
<span class="agreement-link" @click.stop.prevent="openPrivacy">隐私政策</span>
</el-checkbox>
</div>
</el-form-item>
<el-form-item>
<el-button class="login-btn" size="large" type="primary" @click="login"
>登录</el-button
@@ -65,13 +75,14 @@ import { showMessageError } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import MarkdownIt from 'markdown-it'
import AccountTop from '@/components/AccountTop.vue'
import Captcha from '@/components/Captcha.vue'
const router = useRouter()
const title = ref('')
const logo = ref('')
const licenseConfig = ref({})
const wechatLoginURL = ref('')
@@ -81,11 +92,22 @@ const ruleFormRef = ref(null)
const ruleForm = reactive({
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS,
agreement: false,
})
const rules = {
username: [{ required: true, trigger: 'blur', message: '请输入账号' }],
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
agreement: [{ required: true, trigger: 'change', message: '请同意用户协议' }],
}
const agreementContent = ref('')
const privacyContent = ref('')
// 初始化markdown解析器
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
})
onMounted(() => {
// 检查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(
`<div class="markdown-content">${md.render(agreementContent.value)}</div>`,
'用户协议',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {}
}
)
}
const openPrivacy = () => {
// 使用弹窗显示隐私政策内容支持Markdown格式
ElMessageBox.alert(
`<div class="markdown-content">${md.render(privacyContent.value)}</div>`,
'隐私政策',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {}
}
)
}
</script>
<style lang="stylus" scoped>
@import "@/assets/css/login.styl"
.agreement-box
margin-bottom: 10px
transition: all 0.3s
.agreement-link
color: var(--el-color-primary)
cursor: pointer
.agreement-error
.el-checkbox
.el-checkbox__input
.el-checkbox__inner
border-color: #F56C6C !important
.shake
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both
@keyframes shake
10%, 90%
transform: translate3d(-1px, 0, 0)
20%, 80%
transform: translate3d(2px, 0, 0)
30%, 50%, 70%
transform: translate3d(-4px, 0, 0)
40%, 60%
transform: translate3d(4px, 0, 0)
</style>
<style>
/* 全局样式用于Markdown内容显示 */
.markdown-content {
text-align: left;
max-height: 60vh;
overflow-y: auto;
padding: 10px;
}
.markdown-content h1 {
font-size: 1.5em;
margin-bottom: 15px;
}
.markdown-content h2 {
font-size: 1.3em;
margin: 15px 0 10px;
}
.markdown-content p {
margin-bottom: 10px;
line-height: 1.5;
}
.markdown-content ul, .markdown-content ol {
padding-left: 20px;
margin-bottom: 10px;
}
.markdown-content blockquote {
border-left: 4px solid #ccc;
padding-left: 10px;
color: #666;
margin: 10px 0;
}
.markdown-content code {
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.markdown-content pre {
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>

View File

@@ -62,6 +62,17 @@
<el-input placeholder="请输入邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off"> </el-input>
</el-form-item>
<el-form-item label="" prop="agreement" :class="{'agreement-error': agreementError}">
<div class="agreement-box" :class="{'shake': isShaking}">
<el-checkbox v-model="data.agreement" @change="handleAgreementChange">
我已阅读并同意
<span class="agreement-link" @click.stop.prevent="openAgreement">用户协议</span>
<span class="agreement-link" @click.stop.prevent="openPrivacy">隐私政策</span>
</el-checkbox>
</div>
</el-form-item>
<el-row class="btn-row" :gutter="20">
<el-col :span="24">
<el-button class="login-btn" type="primary" size="large" @click="submitRegister">注册</el-button>
@@ -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(
`<div class="markdown-content">${md.render(agreementContent.value)}</div>`,
'用户协议',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {}
}
);
};
const openPrivacy = () => {
// 使用弹窗显示隐私政策内容支持Markdown格式
ElMessageBox.alert(
`<div class="markdown-content">${md.render(privacyContent.value)}</div>`,
'隐私政策',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {}
}
);
};
</script>
<style lang="stylus" scoped>
@@ -243,4 +336,83 @@ const doSubmitRegister = (verifyData) => {
margin-top: 20px
}
.agreement-box
margin-bottom: 10px
transition: all 0.3s
.agreement-link
color: var(--el-color-primary)
cursor: pointer
.agreement-error
.el-checkbox
.el-checkbox__input
.el-checkbox__inner
border-color: #F56C6C !important
.shake
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both
@keyframes shake
10%, 90%
transform: translate3d(-1px, 0, 0)
20%, 80%
transform: translate3d(2px, 0, 0)
30%, 50%, 70%
transform: translate3d(-4px, 0, 0)
40%, 60%
transform: translate3d(4px, 0, 0)
</style>
<style>
/* 全局样式用于Markdown内容显示 */
.markdown-content {
text-align: left;
max-height: 60vh;
overflow-y: auto;
padding: 10px;
}
.markdown-content h1 {
font-size: 1.5em;
margin-bottom: 15px;
}
.markdown-content h2 {
font-size: 1.3em;
margin: 15px 0 10px;
}
.markdown-content p {
margin-bottom: 10px;
line-height: 1.5;
}
.markdown-content ul, .markdown-content ol {
padding-left: 20px;
margin-bottom: 10px;
}
.markdown-content blockquote {
border-left: 4px solid #ccc;
padding-left: 10px;
color: #666;
margin: 10px 0;
}
.markdown-content code {
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.markdown-content pre {
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>

View File

@@ -311,6 +311,25 @@
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="用户协议" name="agreement">
<md-editor class="mgb20" v-model="agreement" :theme="store.theme" @on-upload-img="onUploadImg" />
<el-form-item>
<div style="padding-top: 10px; margin-left: 150px">
<el-button type="primary" @click="save('agreement')">保存</el-button>
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="隐私声明" name="privacy">
<md-editor class="mgb20" v-model="privacy" :theme="store.theme" @on-upload-img="onUploadImg" />
<el-form-item>
<div style="padding-top: 10px; margin-left: 150px">
<el-button type="primary" @click="save('privacy')">保存</el-button>
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="思维导图" name="mark_map">
<md-editor class="mgb20" :theme="store.theme" v-model="system['mark_map_text']" @on-upload-img="onUploadImg" />
<el-form-item>
@@ -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);
});
}
};