Files
zacfrontuser_v2/src/components/BindPhoneDialog.vue
2026-01-15 18:03:13 +08:00

420 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, nextTick, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useDialogStore } from "@/stores/dialogStore";
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { showToast } from "vant";
import useApiFetch from "@/composables/useApiFetch";
import { registerByInviteCode } from "@/api/agent";
const emit = defineEmits(['register-success'])
const router = useRouter();
const route = useRoute();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
const appName = import.meta.env.VITE_APP_NAME || '真爱查';
const phoneNumber = ref("");
const verificationCode = ref("");
const inviteCode = ref("");
const isCountingDown = ref(false);
const countdown = ref(60);
const isAgreed = ref(false);
const hasAccount = ref(false); // 是否有平台账号
let timer = null;
// 聚焦状态变量
const phoneFocused = ref(false);
const codeFocused = ref(false);
const inviteFocused = ref(false);
// 从URL参数中读取邀请码并自动填入
onMounted(() => {
const inviteCodeParam = route.query.invite_code;
if (inviteCodeParam) {
inviteCode.value = inviteCodeParam;
}
// 如果用户已登录且有手机号,自动填充手机号
const token = localStorage.getItem("token");
if (token && userStore.mobile) {
phoneNumber.value = userStore.mobile;
}
});
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
});
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim().length > 0;
});
const canRegister = computed(() => {
if (hasAccount.value) {
// 已有账号模式:只需要手机号和验证码
return (
isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isAgreed.value
);
} else {
// 新注册模式:需要手机号、验证码和邀请码
return (
isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isInviteCodeValid.value &&
isAgreed.value
);
}
});
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return;
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请先输入邀请码" });
return;
}
const actionType = hasAccount.value ? "bindMobile" : "agentApply";
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('registerVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg);
}
}
}
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 (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" });
return;
}
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请输入邀请码" });
return;
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
return;
}
try {
if (hasAccount.value) {
// 已有账号模式:绑定手机号登录
const { data, error } = await useApiFetch("/user/bindMobile")
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "绑定成功!" });
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
closeDialog();
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 发出注册成功的事件
emit('register-success');
// 检查是否是代理,如果是代理跳转到代理主页,否则跳转到首页
setTimeout(() => {
if (agentStore.isAgent) {
router.replace("/agent");
} else {
router.replace("/");
}
}, 300);
} else {
// 检查是否是手机号已绑定其他微信的错误
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
} else {
showToast(data.value.msg || "绑定失败,请重试");
}
}
}
} else {
// 新注册模式:通过邀请码注册成为代理
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) {
showToast({ message: "注册成功!" });
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 || '普通代理'
});
}
closeDialog();
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 发出注册成功的事件
emit('register-success');
// 跳转到代理主页
setTimeout(() => {
router.replace("/agent");
}, 300);
} else {
// 检查是否是手机号已绑定其他微信的错误
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
} else {
showToast(data.value.msg || "注册失败,请重试");
}
}
}
}
} catch (err) {
console.error('操作失败:', err);
showToast({ message: "操作失败,请重试" });
}
}
function closeDialog() {
dialogStore.closeRegisterAgent();
// 重置表单
phoneNumber.value = "";
verificationCode.value = "";
inviteCode.value = "";
isAgreed.value = false;
hasAccount.value = false;
if (timer) {
clearInterval(timer);
}
}
function toUserAgreement() {
closeDialog();
router.push(`/userAgreement`);
}
function toPrivacyPolicy() {
closeDialog();
router.push(`/privacyPolicy`);
}
</script>
<template>
<div v-if="dialogStore.showRegisterAgent">
<van-popup v-model:show="dialogStore.showRegisterAgent" round position="bottom" :style="{ height: '85%' }"
@close="closeDialog">
<div class="register-agent-dialog">
<div class="title-bar">
<div class="font-bold">注册成为代理</div>
<div class="text-sm text-gray-500 mt-1">
{{ hasAccount ? '绑定手机号登录已有账号' : '请输入手机号和邀请码完成代理注册' }}
</div>
<van-icon name="cross" class="close-icon" @click="closeDialog" />
</div>
<div class="px-8">
<div class="mb-8 pt-8 text-left">
<div class="flex flex-col items-center">
<img class="h-16 w-16 rounded-full shadow" src="@/assets/images/logo.jpg" alt="Logo" />
<div class="text-3xl mt-4 text-slate-700 font-bold">
{{ appName }}
</div>
</div>
</div>
<div class="space-y-5">
<!-- 账号类型选择 -->
<div class="flex items-center space-x-4 mb-4">
<button @click="hasAccount = false" :class="[
'flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all',
!hasAccount
? 'bg-blue-500 text-white shadow-md'
: 'bg-gray-100 text-gray-600'
]">
新注册
</button>
<button @click="hasAccount = true" :class="[
'flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all',
hasAccount
? 'bg-blue-500 text-white shadow-md'
: 'bg-gray-100 text-gray-600'
]">
已有平台账号
</button>
</div>
<!-- 重要提示 -->
<div v-if="hasAccount" class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4">
<div class="flex items-start">
<van-icon name="warning-o" class="text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
<div class="text-xs text-yellow-800 leading-relaxed">
<p class="font-semibold mb-1">重要提示</p>
<p> 一个微信只能绑定一个手机号</p>
<p> 如果该手机号已绑定其他微信号将无法在此微信登录</p>
<p> 请确保输入的是您已注册的手机号</p>
</div>
</div>
</div>
<!-- 邀请码输入仅新注册模式显示 -->
<div v-if="!hasAccount" :class="[
'input-container bg-blue-300/20',
inviteFocused ? 'focused' : '',
]">
<input v-model="inviteCode" class="input-field" type="text" placeholder="请输入邀请码"
@focus="inviteFocused = true" @blur="inviteFocused = false" />
</div>
<!-- 手机号输入 -->
<div :class="[
'input-container bg-blue-300/20',
phoneFocused ? 'focused' : '',
]">
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号"
maxlength="11" @focus="phoneFocused = true" @blur="phoneFocused = false" />
</div>
<!-- 验证码输入 -->
<div class="flex items-center justify-between">
<div :class="[
'input-container bg-blue-300/20',
codeFocused ? 'focused' : '',
]">
<input v-model="verificationCode" id="registerVerificationCode" class="input-field"
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
@blur="codeFocused = false" />
</div>
<button
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
:class="isCountingDown || !isPhoneNumberValid || (!hasAccount && !isInviteCodeValid)
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
</div>
<!-- 协议同意框 -->
<div class="flex items-start space-x-2">
<input type="checkbox" v-model="isAgreed" class="mt-1" />
<span class="text-xs text-gray-400 leading-tight">
注册成为代理即代表您已阅读并同意
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
用户协议
</a>
<a class="cursor-pointer text-blue-400" @click="toPrivacyPolicy">
隐私政策
</a>
</span>
</div>
</div>
<button
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
:class="{ 'opacity-50 cursor-not-allowed': !canRegister }" @click="handleRegister">
{{ hasAccount ? '绑定手机号登录' : '注册成为代理' }}
</button>
</div>
</div>
</van-popup>
</div>
</template>
<style scoped>
.register-agent-dialog {
background: url("@/assets/images/login_bg.png") no-repeat;
background-position: center;
background-size: cover;
height: 100%;
}
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.close-icon {
font-size: 20px;
color: #666;
cursor: pointer;
}
.input-container {
border: 2px solid rgba(125, 211, 252, 0);
border-radius: 1rem;
transition: duration-200;
}
.input-container.focused {
border: 2px solid #3b82f6;
}
.input-field {
width: 100%;
padding: 1rem;
background: transparent;
border: none;
outline: none;
transition: border-color 0.3s ease;
}
</style>