mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-02-16 19:34:28 +08:00
Merge branch 'dev-4.2.4' of gitee.com:blackfox/geekai-plus into dev-4.2.4
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
## v4.2.4
|
||||
- 功能新增:管理后台支持设置默认昵称
|
||||
- 功能优化:支持 Suno v4.5 模型支持
|
||||
- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。
|
||||
- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文
|
||||
|
||||
## v4.2.3
|
||||
|
||||
|
||||
3
database/update-v4.2.3.1.sql
Normal file
3
database/update-v4.2.3.1.sql
Normal 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}');
|
||||
@@ -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
12
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="content position-relative">
|
||||
<div v-html="content"></div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
|
||||
@@ -71,7 +73,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="content position-relative">
|
||||
<div v-html="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
@@ -88,7 +92,7 @@
|
||||
import { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { dateFormat, isImage, processPrompt } from '@/utils/libs'
|
||||
import { Clock } from '@element-plus/icons-vue'
|
||||
import { Clock, Edit } from '@element-plus/icons-vue'
|
||||
import hl from 'highlight.js'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
@@ -144,6 +148,9 @@ const finalTokens = ref(props.data.tokens)
|
||||
const content = ref(processPrompt(props.data.content))
|
||||
const files = ref([])
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['edit'])
|
||||
|
||||
onMounted(() => {
|
||||
processFiles()
|
||||
})
|
||||
@@ -475,4 +482,39 @@ const isExternalImg = (link, files) => {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.operations
|
||||
display none
|
||||
position absolute
|
||||
right 5px
|
||||
top 5px
|
||||
|
||||
.text-box
|
||||
&:hover
|
||||
.operations
|
||||
display flex
|
||||
gap 5px
|
||||
|
||||
.op-edit
|
||||
cursor pointer
|
||||
color #409eff
|
||||
font-size 16px
|
||||
|
||||
&:hover
|
||||
color darken(#409eff, 10%)
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content:hover .action-buttons {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly" class="flex">
|
||||
<span class="bar-item" @click="reGenerate(data.prompt)">
|
||||
<span class="bar-item" @click="reGenerate()">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
@@ -92,7 +92,7 @@
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly" class="flex">
|
||||
<span class="bar-item bg" @click="reGenerate(data.prompt)">
|
||||
<span class="bar-item bg" @click="reGenerate()">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
@@ -233,9 +233,8 @@ const stopSynthesis = () => {
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = (prompt) => {
|
||||
console.log(prompt)
|
||||
emits('regen', prompt)
|
||||
const reGenerate = () => {
|
||||
emits('regen')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
<welcome @send="autofillPrompt" />
|
||||
</div>
|
||||
<div v-for="item in chatData" :key="item.id" v-else>
|
||||
<chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle" />
|
||||
<chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle" @edit="editUserPrompt" />
|
||||
<chat-reply
|
||||
v-else-if="item.type === 'reply'"
|
||||
:data="item"
|
||||
@@ -1189,30 +1189,108 @@ const stopGenerate = function () {
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = function (prompt) {
|
||||
disableInput(false)
|
||||
const text = '重新回答下述问题:' + prompt
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: 'prompt',
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: text,
|
||||
})
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
chat_id: chatId.value,
|
||||
content: text,
|
||||
tools: toolSelected.value,
|
||||
stream: stream.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
const reGenerate = function () {
|
||||
// 恢复发送按钮状态
|
||||
canSend.value = true;
|
||||
showStopGenerate.value = false;
|
||||
|
||||
// 查找最后的用户消息和AI回复并删除
|
||||
if (chatData.value.length >= 2) {
|
||||
// 从后往前找,如果最后一条是AI回复,再往前一条是用户消息
|
||||
if (chatData.value[chatData.value.length - 1].type === 'reply') {
|
||||
// 删除AI回复
|
||||
chatData.value.pop();
|
||||
|
||||
// 如果此时最后一条是用户消息,也删除它
|
||||
if (chatData.value.length > 0 && chatData.value[chatData.value.length - 1].type === 'prompt') {
|
||||
// 保存用户消息内容,填入输入框
|
||||
const userPrompt = chatData.value[chatData.value.length - 1].content;
|
||||
// 删除用户消息
|
||||
chatData.value.pop();
|
||||
// 填入输入框
|
||||
prompt.value = userPrompt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将光标定位到输入框并聚焦
|
||||
nextTick(() => {
|
||||
if (inputRef.value) {
|
||||
inputRef.value.focus();
|
||||
// 触发输入事件以更新文本高度
|
||||
onInput({ keyCode: null });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑用户消息
|
||||
const editUserPrompt = function (messageId) {
|
||||
// 找到要编辑的消息及其索引
|
||||
let messageIndex = -1;
|
||||
let messageContent = '';
|
||||
|
||||
for (let i = 0; i < chatData.value.length; i++) {
|
||||
if (chatData.value[i].id === messageId) {
|
||||
messageIndex = i;
|
||||
messageContent = chatData.value[i].content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (messageIndex === -1) return;
|
||||
|
||||
// 弹出编辑对话框
|
||||
ElMessageBox.prompt('', '编辑消息', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: messageContent,
|
||||
inputType: 'textarea',
|
||||
customClass: 'edit-prompt-dialog',
|
||||
roundButton: true
|
||||
}).then(({ value }) => {
|
||||
if (value.trim() === '') {
|
||||
ElMessage.warning('消息内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新用户消息
|
||||
chatData.value[messageIndex].content = value;
|
||||
|
||||
// 移除该消息之后的所有消息
|
||||
chatData.value = chatData.value.slice(0, messageIndex + 1);
|
||||
|
||||
// 添加空回复消息
|
||||
const _role = getRoleById(roleId.value);
|
||||
chatData.value.push({
|
||||
chat_id: chatId,
|
||||
role_id: roleId.value,
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: '',
|
||||
});
|
||||
|
||||
disableInput(false);
|
||||
|
||||
// 发送编辑后的消息
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
chat_id: chatId.value,
|
||||
content: value,
|
||||
tools: toolSelected.value,
|
||||
stream: stream.value,
|
||||
edit_message: true
|
||||
},
|
||||
})
|
||||
);
|
||||
}).catch(() => {
|
||||
// 取消编辑
|
||||
});
|
||||
}
|
||||
|
||||
const chatName = ref('')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user