foreign-admin-ui/src/views/user/Register.vue

360 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>