360 lines
11 KiB
Vue
360 lines
11 KiB
Vue
<template>
|
||
<div class="login-wrapper">
|
||
<div class="user-card ">
|
||
<UserHeader />
|
||
<UserText :user-text="userText" />
|
||
<UserTabs v-model="activeTab" :tab-list="tabList" />
|
||
|
||
<el-form class="user-form" ref="registerFormRef" :model="registerForm" :rules="registerRules">
|
||
<Transition name="form-switch" mode="out-in">
|
||
<el-form-item class="captcha-item" v-if="activeTab === 'email'" key="form-email-item" prop="email">
|
||
<el-input v-model="registerForm.email" placeholder="请输入邮箱" clearable size="large" />
|
||
<el-button type="primary" class="captcha-btn" @click="handleSendCode" :disabled="emailIsCounting">{{
|
||
emailIsCounting ? `${emailCountdown}s后重新获取` : '发送验证码' }}</el-button>
|
||
</el-form-item>
|
||
|
||
<el-form-item class="phone-item captcha-item" prop="phone" v-else-if="activeTab === 'phone'"
|
||
key="form-phone-item">
|
||
<el-select v-model="registerForm.countryCode" :teleported="false" value-key="code" placeholder="国家"
|
||
size="large">
|
||
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
|
||
<div class="country-option">
|
||
<span class="country-name">{{ item.name }}</span>
|
||
<span class="country-code">{{ item.code }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
<el-input v-model="registerForm.phone" placeholder="请输入手机号" clearable size="large" />
|
||
<el-button type="primary" class="captcha-btn" @click="handleSendCode" :disabled="phoneIsCounting">{{
|
||
phoneIsCounting ? `${phoneCountdown}s后重新获取` : '发送验证码' }}</el-button>
|
||
</el-form-item>
|
||
</Transition>
|
||
|
||
<UserCaptcha v-model="registerForm.code" />
|
||
<el-form-item label="设置密码(6-12位密码)" label-position="top" prop="password">
|
||
<el-input placeholder="请输入密码" clearable show-password size="large" type="password"
|
||
v-model="registerForm.password" />
|
||
</el-form-item>
|
||
<el-form-item label="邀请链接/邀请码(选填)" label-position="top" prop="inviteCode">
|
||
<el-input placeholder="请输入邀请链接/邀请码" clearable size="large" type="text" v-model="registerForm.inviteCode" />
|
||
</el-form-item>
|
||
<el-form-item prop="agreeAgreement">
|
||
<div style="display: flex;align-items: center; justify-content: flex-start;">
|
||
<el-checkbox v-model="registerForm.agreeAgreement"></el-checkbox>
|
||
<div style="font-size: 13px;display: flex;flex-wrap: wrap;margin-left: 8px;line-height: 17px;">
|
||
我已阅读并同意
|
||
<span class="doc-link" @click="openDoc('服务条款.docx', '服务条款')">《服务条款》</span>、
|
||
<span class="doc-link" @click="openDoc('隐私协议.docx', '隐私协议')">《隐私协议》</span>和
|
||
<span class="doc-link" @click="openDoc('风险提示.docx', '风险提示')">《风险提示》</span>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item class="form-submit">
|
||
<el-button type="primary" class="form-btn" @click="handleRegister" :loading="buttonLoading">注册</el-button>
|
||
</el-form-item>
|
||
<router-link to="/user/login" class="to-login">已有账号?立即登录</router-link>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 协议预览弹窗 -->
|
||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="70%" top="3vh">
|
||
<div class="doc-preview" v-html="docHtml"></div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||
<el-button type="primary" @click="downloadCurrentDoc">下载文档</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import mammoth from 'mammoth'
|
||
import { ref, onUnmounted, onMounted } from 'vue'
|
||
import termFile from '@/assets/file/服务条款.docx?url'
|
||
import privacyFile from '@/assets/file/隐私协议.docx?url'
|
||
import riskFile from '@/assets/file/风险提示.docx?url'
|
||
defineOptions({
|
||
name: 'Register'
|
||
})
|
||
// type类型
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { ElMessage } from 'element-plus'
|
||
// 组件
|
||
import UserHeader from './components/Header.vue'
|
||
import UserText from './components/Text.vue'
|
||
import UserTabs from './components/Tabs.vue'
|
||
import UserCaptcha from './components/Captcha.vue'
|
||
|
||
//公共方法/store/配置文件
|
||
import { useUserStore } from '@/stores/modules/user'
|
||
import { md5Encrypt } from '@/utils/crypto'
|
||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
||
import { useButtonLoading } from '@/composables/useLoading'
|
||
|
||
//用户校验规则
|
||
import { countryOptions } from '@/views/user/config/mock'
|
||
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
|
||
import { useRoute } from 'vue-router'
|
||
|
||
//store实例
|
||
const userStore = useUserStore()
|
||
const route = useRoute()
|
||
|
||
// 弹窗相关
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('')
|
||
const docHtml = ref('')
|
||
let currentDocName = ref('')
|
||
|
||
// 按钮 loading
|
||
const { buttonLoading, startButtonLoading, stopButtonLoading } = useButtonLoading()
|
||
|
||
// UserText 实例
|
||
const userText = ref({
|
||
backText: '',
|
||
title: '注册',
|
||
text: '',
|
||
})
|
||
|
||
//tabs 实例
|
||
const tabList = ref([
|
||
{ label: '邮箱', name: 'email' },
|
||
{ label: '手机', name: 'phone' },
|
||
])
|
||
const activeTab = ref<string>('email')
|
||
|
||
// 注册表单实例
|
||
const registerFormRef = ref<FormInstance>()
|
||
const registerForm = ref<any>({
|
||
email: '',
|
||
countryCode: '+86',
|
||
phone: '',
|
||
password: '',
|
||
code: '',
|
||
inviteCode: '',
|
||
agreeAgreement: false,
|
||
})
|
||
const registerRules = ref<FormRules>({
|
||
email: emailRules().email,
|
||
phone: phoneRules(registerForm.value).phone,
|
||
password: passwordRules(registerForm.value).password,
|
||
code: codeRules().code,
|
||
agreeAgreement: [
|
||
{
|
||
validator: (rule: any, value: any, callback: any) => {
|
||
if (!value) {
|
||
callback(new Error('请勾选同意注册协议'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
})
|
||
|
||
//初始化验证码倒计时状态(添加页面前缀隔离不同页面的倒计时状态)
|
||
const emailCaptchaState = initCaptchaState('email', 60, 'register_email') // 邮箱专属
|
||
const phoneCaptchaState = initCaptchaState('phone', 60, 'register_phone') // 手机专属
|
||
// 邮箱验证码状态
|
||
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
|
||
// 手机验证码状态
|
||
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
|
||
|
||
const fileMap: Record<string, string> = {
|
||
'服务条款.docx': termFile,
|
||
'隐私协议.docx': privacyFile,
|
||
'风险提示.docx': riskFile,
|
||
}
|
||
|
||
// 打开并解析docx弹窗预览
|
||
|
||
async function openDoc(fileName: string, title: string) {
|
||
currentDocName.value = fileName
|
||
dialogTitle.value = title
|
||
dialogVisible.value = true
|
||
docHtml.value = '加载中...'
|
||
const url = fileMap[fileName]
|
||
try {
|
||
const res = await fetch(url)
|
||
const buf = await res.arrayBuffer()
|
||
const ret = await mammoth.convertToHtml({ arrayBuffer: buf })
|
||
docHtml.value = ret.value
|
||
} catch {
|
||
docHtml.value = '加载失败,请下载查看'
|
||
}
|
||
}
|
||
|
||
// 下载当前打开的文档
|
||
|
||
function downloadCurrentDoc() {
|
||
const url = fileMap[currentDocName.value]
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = currentDocName.value
|
||
a.click()
|
||
}
|
||
|
||
//方法
|
||
|
||
// 定义发送验证码接口
|
||
const sendCodeApi = async () => {
|
||
// 发送验证码参数
|
||
const sendParams = {
|
||
email: registerForm.value.email,
|
||
mobile: registerForm.value.countryCode + registerForm.value.phone,
|
||
mobile_area: registerForm.value.countryCode,
|
||
type: 'reg_code',
|
||
}
|
||
return userStore.sendCode(sendParams)
|
||
}
|
||
//发送验证码
|
||
const handleSendCode = async () => {
|
||
if (!registerFormRef.value) return
|
||
try {
|
||
await registerFormRef.value.validateField(activeTab.value)
|
||
} catch (error) {
|
||
console.error('字段校验失败:', error)
|
||
return
|
||
}
|
||
|
||
// 确定类型、接收方、对应的倒计时状态和发送函数
|
||
const type: string = activeTab.value === 'phone' ? 'phone' : 'email'
|
||
const receiver =
|
||
type === 'phone'
|
||
? registerForm.value.countryCode + registerForm.value.phone
|
||
: registerForm.value.email
|
||
const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState
|
||
const sendFunc = type === 'phone' ? 'sendPhoneCaptcha' : 'sendEmailCaptcha'
|
||
console.log('registerForm.countryCode', registerForm.value.countryCode)
|
||
// 调用封装的发送函数(传正确的函数,而非字符串)
|
||
await sendCaptcha(
|
||
type,
|
||
receiver,
|
||
currentCaptchaState, // 只更新当前类型的倒计时状态
|
||
() => sendCodeApi(), // 传入返回Promise的函数
|
||
)
|
||
}
|
||
|
||
//注册
|
||
const handleRegister = async () => {
|
||
if (!registerFormRef.value) return
|
||
|
||
// 先检查协议勾选状态
|
||
if (!registerForm.value.agreeAgreement) {
|
||
ElMessage.error('请勾选同意注册协议')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 提交时再次验证(显示错误提示)
|
||
await registerFormRef.value.validate()
|
||
// 开启 loading
|
||
startButtonLoading()
|
||
try {
|
||
// 注册参数
|
||
const registerParams = {
|
||
email: registerForm.value.email,
|
||
mobile_area: registerForm.value.countryCode,
|
||
mobile: registerForm.value.countryCode + registerForm.value.phone,
|
||
reg_type: activeTab.value === 'email' ? 1 : 2,
|
||
invite_code: registerForm.value.inviteCode,
|
||
code: registerForm.value.code,
|
||
password: registerForm.value.password,
|
||
}
|
||
|
||
// 注册逻辑
|
||
await userStore.register(registerParams)
|
||
} finally {
|
||
// 关闭 loading
|
||
stopButtonLoading()
|
||
}
|
||
} catch (error) {
|
||
console.error('表单校验失败:', error)
|
||
}
|
||
}
|
||
// 组件卸载时清除定时器(关键:防止内存泄漏)
|
||
onUnmounted(() => {
|
||
clearCaptchaTimer(emailCaptchaState)
|
||
clearCaptchaTimer(phoneCaptchaState)
|
||
})
|
||
|
||
onMounted(() => {
|
||
registerForm.value.inviteCode = route.query.invite_code || ''
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.login-wrapper {
|
||
min-height: 100vh;
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: radial-gradient(ellipse at center, #f0f7ff 0%, #e8f4fd 40%, #f5f9ff 100%);
|
||
}
|
||
|
||
a {
|
||
text-decoration: none;
|
||
color: var(--el-color-primary);
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.doc-link {
|
||
color: var(--el-color-primary);
|
||
cursor: pointer;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.user-form {
|
||
.phone-item,
|
||
.captcha-item {
|
||
:deep(.el-form-item__content) {
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
:deep(.el-select-dropdown__item) {
|
||
padding: 0 16px 0 16px;
|
||
}
|
||
|
||
.el-select {
|
||
width: 180px !important;
|
||
}
|
||
}
|
||
|
||
.to-login {
|
||
display: flex;
|
||
justify-content: center;
|
||
text-decoration: none;
|
||
color: #409eff;
|
||
margin-top: 24px;
|
||
}
|
||
}
|
||
</style>
|
||
<style lang="scss">
|
||
/* 弹窗预览全局样式,不要scoped */
|
||
.doc-preview {
|
||
max-height: 65vh;
|
||
overflow-y: auto;
|
||
line-height: 1.8;
|
||
font-size: 15px;
|
||
padding: 10px;
|
||
h1, h2, h3, h4 {
|
||
margin: 16px 0 8px;
|
||
}
|
||
p {
|
||
margin: 8px 0;
|
||
}
|
||
table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 12px 0;
|
||
td, th {
|
||
border: 1px solid #ccc;
|
||
padding: 6px 10px;
|
||
}
|
||
}
|
||
}
|
||
</style> |