Files
zacfrontuser_v2/src/views/Register.vue
2026-02-26 14:58:46 +08:00

554 lines
16 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.

<script setup>
import { ref, computed, onUnmounted, onMounted, nextTick } from 'vue'
import { showToast } from 'vant'
import { registerByInviteCode, applyForAgent } from '@/api/agent'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useRoute, useRouter } from 'vue-router'
import useApiFetch from '@/composables/useApiFetch'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
const phoneNumber = ref('')
const verificationCode = ref('')
const inviteCode = ref('')
const isAgreed = ref(false)
const isCountingDown = ref(false)
const countdown = ref(60)
const isPhoneDisabled = ref(false)
let timer = null
// 填充默认邀请码
function fillInviteCode() {
inviteCode.value = '16800'
}
// 从URL参数中读取邀请码并自动填入如果用户已登录且有手机号则自动填充
onMounted(async () => {
const inviteCodeParam = route.query.invite_code
if (inviteCodeParam) {
inviteCode.value = inviteCodeParam
}
// 从路由参数获取手机号(已注册用户继续注册成为代理的情况)
const mobileParam = route.query.mobile
if (mobileParam) {
phoneNumber.value = mobileParam
isPhoneDisabled.value = true
} else {
// 如果用户已登录且有手机号,自动填充手机号
const token = localStorage.getItem('token')
if (token) {
if (!userStore.mobile) {
await userStore.fetchUserInfo()
}
if (userStore.mobile) {
phoneNumber.value = userStore.mobile
isPhoneDisabled.value = true
}
}
}
})
const isRegisteredUser = ref(false)
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
})
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim().length > 0
})
const canRegister = computed(() => {
return isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isInviteCodeValid.value &&
isAgreed.value
})
// 发送验证码(参考登录页逻辑,使用阿里云滑块验证码)
async function sendVerificationCode() {
// 1. 基础校验
if (isCountingDown.value) return
if (!isPhoneNumberValid.value) {
showToast({ message: '请输入有效的手机号' })
return
}
if (!isInviteCodeValid.value) {
showToast({ message: '请先输入邀请码' })
return
}
// 2. 使用阿里云滑块验证码发送短信
try {
const result = await runWithCaptcha(
// 第一个参数:调用发送短信接口的函数,传入滑块验证参数
(captchaVerifyParam) =>
useApiFetch('auth/sendSms')
.post({
mobile: phoneNumber.value,
actionType: 'agentApply', // 根据业务选择 login 或 agentApply
captchaVerifyParam,
inviteCode: inviteCode.value.trim() // 带上邀请码,后端可能需要校验
})
.json(),
// 第二个参数:滑块验证成功后,处理短信发送结果的回调
(res) => {
if (res.code === 200) {
showToast({ message: '验证码发送成功' })
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode')
if (verificationCodeInput) {
verificationCodeInput.focus()
}
})
return true // 告诉 runWithCaptcha 验证+发送成功
} else {
showToast({ message: res.msg || '验证码发送失败' })
return false // 告诉 runWithCaptcha 失败,不再继续
}
}
)
// 如果滑块验证或短信发送失败,直接返回
if (!result) {
return
}
} catch (error) {
console.error('发送验证码失败:', error)
showToast({ message: '发送验证码失败,请重试' })
}
}
function startCountdown() {
isCountingDown.value = true
countdown.value = 60
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
} else {
clearInterval(timer)
isCountingDown.value = false
}
}, 1000)
}
async function handleRegister() {
if (!isPhoneNumberValid.value) {
showToast({ message: '请输入有效的手机号' })
return
}
if (!isInviteCodeValid.value) {
showToast({ message: '请输入邀请码' })
return
}
if (verificationCode.value.length !== 6) {
showToast({ message: '请输入有效的验证码' })
return
}
if (!isAgreed.value) {
showToast({ message: '请先同意用户协议、隐私政策和代理管理协议' })
return
}
performRegister()
}
// 执行实际的注册逻辑
async function performRegister() {
try {
const { data, error } = await registerByInviteCode({
mobile: phoneNumber.value,
code: verificationCode.value,
referrer: inviteCode.value.trim()
})
if (data.value && !error.value) {
if (data.value.code === 200) {
// 保存token
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
// 更新代理信息到store
if (data.value.data.agent_id) {
agentStore.updateAgentInfo({
isAgent: true,
agentID: data.value.data.agent_id,
level: data.value.data.level || 1,
levelName: data.value.data.level_name || '普通代理'
})
}
showToast({ message: '注册成功!' })
setTimeout(() => {
window.location.href = '/'
}, 500)
} else {
showToast(data.value.msg || '注册失败,请重试')
}
}
} catch (err) {
console.error('注册失败:', err)
showToast({ message: '注册失败,请重试' })
}
}
// 已注册用户申请成为代理
async function applyForAgentAsRegisteredUser() {
try {
const { data, error } = await applyForAgent({
mobile: phoneNumber.value,
code: verificationCode.value,
referrer: inviteCode.value.trim()
})
if (data.value && !error.value) {
if (data.value.code === 200) {
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
showToast({ message: '申请成功!' })
setTimeout(() => {
window.location.href = '/'
}, 500)
} else {
showToast(data.value.msg || '申请失败,请重试')
}
}
} catch (err) {
console.error('申请失败:', err)
showToast({ message: '申请失败,请重试' })
}
}
function toUserAgreement() {
router.push('/userAgreement')
}
function toPrivacyPolicy() {
router.push('/privacyPolicy')
}
function toAgentManageAgreement() {
router.push('/agentManageAgreement')
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
const onClickLeft = () => {
router.replace('/')
}
</script>
<template>
<div class="login-layout">
<van-nav-bar fixed placeholder title="注册成为代理" left-text="" left-arrow @click-left="onClickLeft" />
<div class="login px-4 relative z-10">
<div class="mb-8 pt-20 text-left">
<div class="flex flex-col items-center">
<img class="h-16 w-16 rounded-full shadow" src="@/assets/images/logo.png" alt="Logo" />
<div class="report-title-wrapper">
<h1 class="report-title">大数据风险报告</h1>
<div class="report-title-decoration"></div>
</div>
</div>
</div>
<!-- 注册表单 -->
<div class="login-form">
<!-- 邀请码输入 -->
<div class="form-item">
<div class="form-label">邀请码</div>
<div class="input-with-btn">
<input v-model="inviteCode" class="form-input" type="text" placeholder="请输入邀请码" />
<button class="get-invite-code-btn" @click="fillInviteCode">获取邀请码</button>
</div>
</div>
<!-- 手机号输入 -->
<div class="form-item">
<div class="form-label">手机号</div>
<input v-model="phoneNumber" class="form-input" type="tel" placeholder="请输入手机号" maxlength="11"
:disabled="isPhoneDisabled" />
</div>
<!-- 验证码输入 -->
<div class="form-item">
<div class="form-label">验证码</div>
<div class="verification-input-wrapper">
<input v-model="verificationCode" id="verificationCode" class="form-input verification-input"
placeholder="请输入验证码" maxlength="6" />
<button class="get-code-btn"
:class="{ 'disabled': isCountingDown || !isPhoneNumberValid || !isInviteCodeValid }"
@click="sendVerificationCode"
:disabled="isCountingDown || !isPhoneNumberValid || !isInviteCodeValid">
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</button>
</div>
</div>
<!-- 协议同意框 -->
<div class="agreement-wrapper">
<input type="checkbox" v-model="isAgreed" class="agreement-checkbox accent-primary"
id="agreement" />
<label for="agreement" class="agreement-text">
我已阅读并同意
<a class="agreement-link" @click="toUserAgreement">用户协议</a>
<a class="agreement-link" @click="toPrivacyPolicy">隐私政策</a>
<a class="agreement-link" @click="toAgentManageAgreement">代理管理协议</a>
</label>
</div>
<!-- 提示文字 -->
<div class="notice-text">
未注册手机号注册后将自动生成账号并成为代理并且代表您已阅读并同意
</div>
<!-- 注册按钮 -->
<button class="login-btn" :class="{ 'disabled': !canRegister }" @click="handleRegister"
:disabled="!canRegister">
注册成为代理
</button>
</div>
</div>
</div>
</template>
<style scoped>
/* 样式保持不变,和之前一样 */
.login-layout {
background-image: url('@/assets/images/login_bg.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
position: relative;
overflow: hidden;
}
.login-form {
background-color: var(--color-bg-primary);
padding: 2rem;
margin-top: 0.5rem;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
border-radius: 8px;
}
.form-item {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
border: none;
border-bottom: 1px solid var(--color-border-primary);
}
.input-with-btn {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.input-with-btn .form-input {
padding-right: 6rem;
}
.get-invite-code-btn {
position: absolute;
right: 0;
background: none;
border: none;
color: var(--color-primary);
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
font-weight: 500;
}
.form-label {
font-size: 0.9375rem;
color: var(--color-text-primary);
margin-bottom: 0;
margin-right: 1rem;
font-weight: 500;
min-width: 4rem;
flex-shrink: 0;
}
.form-input {
width: 100%;
padding: 0.875rem 0;
font-size: 0.9375rem;
color: var(--color-text-primary);
outline: none;
background-color: transparent;
}
.form-input::placeholder {
color: var(--color-text-tertiary);
}
.form-input:focus {
border-bottom-color: var(--color-text-primary);
}
.form-input:disabled {
color: var(--color-text-tertiary);
cursor: not-allowed;
background-color: transparent;
}
.verification-input-wrapper {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.verification-input {
flex: 1;
padding-right: 6rem;
}
.get-code-btn {
position: absolute;
right: 0;
background: none;
border: none;
color: var(--color-primary);
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
font-weight: 500;
}
.get-code-btn.disabled {
color: var(--color-gray-400);
cursor: not-allowed;
}
.agreement-wrapper {
display: flex;
align-items: center;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.agreement-checkbox {
flex-shrink: 0;
margin-right: 0.5rem;
}
.agreement-text {
font-size: 0.75rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
.agreement-link {
color: var(--color-primary);
cursor: pointer;
text-decoration: none;
}
.notice-text {
font-size: 0.6875rem;
color: var(--color-text-tertiary);
line-height: 1.5;
margin-bottom: 2rem;
}
.login-btn {
width: 100%;
padding: 0.875rem;
background-color: var(--color-primary);
color: var(--color-text-white);
border: none;
border-radius: 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: opacity 0.3s;
letter-spacing: 0.25rem;
}
.login-btn:hover {
opacity: 0.9;
}
.login-btn.disabled {
background-color: var(--color-gray-300);
cursor: not-allowed;
}
.report-title-wrapper {
position: relative;
margin-top: 1.5rem;
padding: 0 1rem;
}
.report-title {
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #00f2fe 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
letter-spacing: 0.1em;
margin: 0;
padding: 0.5rem 0;
position: relative;
z-index: 1;
animation: gradientShift 3s ease infinite;
text-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.report-title-decoration {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, transparent, #667eea, #764ba2, #667eea, transparent);
border-radius: 2px;
animation: decorationPulse 2s ease-in-out infinite;
}
@keyframes gradientShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes decorationPulse {
0%, 100% {
opacity: 0.6;
transform: translateX(-50%) scaleX(1);
}
50% {
opacity: 1;
transform: translateX(-50%) scaleX(1.2);
}
}
</style>