topfans/frontend/pages/register/register.vue

517 lines
13 KiB
Vue

<template>
<LoginBackground :show-logo="true">
<!-- 内容区域 -->
<view class="register-content">
<!-- 头像 -->
<view class="avatar-wrapper">
<image class="avatar-image" src="/static/login/portal/register-avatar.png" mode="aspectFit"></image>
</view>
<!-- 注册卡片 -->
<view class="register-card">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<!-- 标题 -->
<view class="card-title-wrapper">
<text class="card-title">手机号注册</text>
</view>
<!-- 表单 -->
<view class="form-area">
<!-- 手机号 -->
<view class="input-group">
<view class="input-outer">
<view class="input-inner">
<input class="input-field" type="number" v-model="form.phone" placeholder="输入您的手机号"
placeholder-class="input-placeholder" maxlength="11" @input="handlePhoneInput" />
</view>
</view>
</view>
<!-- 验证码 -->
<view class="input-group code-group">
<view class="input-outer code-outer">
<view class="input-inner code-inner">
<input class="input-field" type="number" v-model="form.code" placeholder="输入验证码"
placeholder-class="input-placeholder" maxlength="6" :disabled="codeStatus === 'verified'" />
</view>
</view>
<view class="send-code-btn" :class="{
countdown: codeStatus === 'countdown',
verified: codeStatus === 'verified',
}" @tap="handleSendCode">
<text v-if="codeStatus === 'unsent' || codeStatus === 'resend'">发送验证码</text>
<text v-else-if="codeStatus === 'countdown'">{{ countdown }}秒</text>
<text v-else-if="codeStatus === 'verified'">已验证</text>
</view>
</view>
<!-- 验证码错误提示 -->
<view v-if="codeError" class="error-message">
<text>{{ codeError }}</text>
</view>
<!-- 创建密码 -->
<view class="input-group">
<view class="input-outer">
<view class="input-inner">
<input class="input-field" :type="showPassword ? 'text' : 'password'" v-model="form.password"
placeholder="创建密码" placeholder-class="input-placeholder" />
</view>
</view>
</view>
<!-- 确认密码 -->
<view class="input-group">
<view class="input-outer">
<view class="input-inner">
<input class="input-field" :type="showPassword ? 'text' : 'password'" v-model="confirmPassword"
placeholder="确认密码" placeholder-class="input-placeholder" />
</view>
</view>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 下一步按钮 -->
<view class="next-btn-wrapper">
<view class="next-btn" @tap="handleRegister">
<text class="next-btn-text">注册</text>
</view>
</view>
</view>
</view>
</LoginBackground>
</template>
<script setup>
import { ref } from "vue";
import LoginBackground from "@/components/LoginBackground.vue";
import { validatePhone, validatePassword } from "@/utils/validator";
import { checkmobileApi, sendCodeApi, verifyCodeApi } from "@/utils/api";
// 响应式数据
const form = ref({
phone: "",
password: "",
code: "",
});
const confirmPassword = ref("");
const showPassword = ref(false);
const errorMessage = ref("");
const phoneChecking = ref(false);
const showCodeInput = ref(true);
const codeStatus = ref("unsent"); // unsent, countdown, resend, verified
const countdown = ref(60);
const codeError = ref("");
const verifyToken = ref("");
const countdownTimer = ref(null);
const isVerifying = ref(false);
const goBack = () => {
uni.reLaunch({ url: "/pages/login/portal" })
};
// 切换密码显示/隐藏
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
// 手机号输入处理
const handlePhoneInput = (e) => {
const phone = e.detail.value;
checkPhoneDuplicate(phone);
};
// 验证手机号是否已注册
let checkPhoneTimer = null;
const checkPhoneDuplicate = async (phone) => {
if (checkPhoneTimer) {
clearTimeout(checkPhoneTimer);
checkPhoneTimer = null;
}
const phoneValidation = validatePhone(phone);
if (!phoneValidation.valid || phone.length !== 11) {
errorMessage.value = "";
return;
}
checkPhoneTimer = setTimeout(async () => {
phoneChecking.value = true;
try {
const res = await checkmobileApi(phone);
if (res.code === 0 && res.data && res.data.exists) {
errorMessage.value = "该手机号已被注册";
} else {
errorMessage.value = "";
}
} catch (error) {
errorMessage.value = "";
} finally {
phoneChecking.value = false;
}
}, 300);
};
// 发送验证码
const handleSendCode = async () => {
const phoneValidation = validatePhone(form.value.phone);
if (!phoneValidation.valid) {
errorMessage.value = phoneValidation.message;
return;
}
try {
const res = await sendCodeApi(form.value.phone, "register");
if (res.code === 0) {
codeStatus.value = "countdown";
countdown.value = res.expires_in || 60;
showCodeInput.value = true;
startCountdown();
uni.showToast({ title: "验证码已发送", icon: "success" });
}
} catch (error) {
codeError.value = error.message || "发送失败,请重试";
}
};
// 倒计时
const startCountdown = () => {
if (countdownTimer.value) {
clearInterval(countdownTimer.value);
}
countdownTimer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(countdownTimer.value);
codeStatus.value = "resend";
}
}, 1000);
};
// 验证验证码
const handleVerifyCode = async () => {
if (!form.value.code || form.value.code.length !== 6) {
codeError.value = "请输入6位验证码";
return;
}
isVerifying.value = true;
codeError.value = "";
try {
const res = await verifyCodeApi(
form.value.phone,
form.value.code,
"register",
);
if (res.code === 0 && res.data && res.data.verified) {
verifyToken.value = res.data.verify_token;
codeStatus.value = "verified";
uni.showToast({ title: "验证成功", icon: "success" });
}
} catch (error) {
codeError.value = error.message || "验证失败";
} finally {
isVerifying.value = false;
}
};
// 注册
const handleRegister = async () => {
const phoneValidation = validatePhone(form.value.phone);
if (!phoneValidation.valid) {
errorMessage.value = phoneValidation.message;
uni.showToast({ title: phoneValidation.message, icon: "none" });
return;
}
const passwordValidation = validatePassword(form.value.password);
if (!passwordValidation.valid) {
errorMessage.value = passwordValidation.message;
uni.showToast({ title: passwordValidation.message, icon: "none" });
return;
}
// 确认密码一致性
if (form.value.password !== confirmPassword.value) {
errorMessage.value = "两次密码输入不一致";
uni.showToast({ title: "两次密码输入不一致", icon: "none" });
return;
}
// 检查验证码
if (!form.value.code || form.value.code.length !== 6) {
errorMessage.value = "请输入6位验证码";
return;
}
if (codeStatus.value !== "verified") {
await handleVerifyCode();
if (codeStatus.value !== "verified") return;
}
errorMessage.value = "";
try {
uni.setStorageSync("temp_register_mobile", form.value.phone);
uni.setStorageSync("temp_register_password", form.value.password);
uni.setStorageSync("temp_register_verify_token", verifyToken.value);
uni.reLaunch({ url: "/pages/profile/setNickname" });
} catch (error) {
errorMessage.value = error.message || "操作失败,请重试";
uni.showToast({ title: errorMessage.value, icon: "none" });
}
};
</script>
<style scoped>
.register-content {
position: relative;
z-index: 10;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
top: -64rpx;
}
/* 头像 */
.avatar-wrapper {
width: 296rpx;
height: 274rpx;
margin-top: 130rpx;
filter: drop-shadow(2rpx 4rpx 98rpx rgba(0, 0, 0, 0.94));
z-index: 11;
}
.avatar-image {
width: 100%;
height: 100%;
}
/* 卡片 */
.register-card {
width: 710rpx;
margin-top: -104rpx;
padding: 30rpx 104rpx 30rpx;
box-sizing: border-box;
border-radius: 56rpx;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(64rpx);
-webkit-backdrop-filter: blur(64rpx);
box-shadow: 0 0 20.5px 0 rgba(0, 0, 0, 0.31);
display: flex;
flex-direction: column;
align-items: center;
z-index: 12;
}
.nav-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 32rpx;
}
.nav-back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
/* 标题 */
.card-title-wrapper {
position: relative;
width: 324rpx;
height: 84rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 56rpx;
background:
linear-gradient(240deg, #cdaffa 9.77%, rgba(205, 175, 250, 0) 62.44%),
linear-gradient(108deg, #f7cbbf 13.91%, rgba(247, 203, 191, 0) 51.34%),
linear-gradient(297deg, #81a9f8 31.28%, rgba(129, 169, 248, 0) 90.74%),
linear-gradient(80deg, #69f4e9 39.86%, rgba(164, 252, 245, 0) 66.88%);
border-radius: 84rpx;
}
.tab-bg {
position: absolute;
inset: 0;
width: 100%;
height: 124%;
}
.card-title {
position: relative;
z-index: 1;
color: #fff9e7;
text-shadow: -1px 1px 4px rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium";
font-size: 46rpx;
font-style: normal;
font-weight: 500;
}
/* 表单 */
.form-area {
width: 100%;
display: flex;
flex-direction: column;
gap: 32rpx;
padding: 0 14rpx;
}
.input-group {
width: 100%;
}
.input-outer {
width: 100%;
height: 74rpx;
border-radius: 58rpx;
background: linear-gradient(90deg,
rgba(255, 222, 8, 0.06) 0%,
rgba(252, 100, 102, 0.12) 64%,
rgba(244, 88, 104, 0.12) 100%);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
padding: 4rpx;
box-sizing: border-box;
}
.input-inner {
width: 100%;
height: 60rpx;
border-radius: 58rpx;
background: rgba(255, 255, 255, 0.65);
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
}
.input-field {
width: 100%;
height: 100%;
font-size: 30rpx;
color: #333;
}
.input-placeholder {
font-size: 30rpx;
color: #565656;
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 验证码行 */
.code-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 12rpx;
}
.code-outer {
flex: 1;
}
.code-inner {
width: 100%;
}
.send-code-btn {
width: 152rpx;
height: 74rpx;
border-radius: 36rpx;
background: linear-gradient(90deg,
rgba(255, 222, 8, 0.06) 0%,
rgba(252, 100, 102, 0.12) 64%,
rgba(244, 88, 104, 0.12) 100%);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
flex-shrink: 0;
}
.send-code-btn.countdown,
.send-code-btn.verified {
opacity: 0.6;
}
/* 错误提示 */
.error-message {
width: 100%;
padding-left: 24rpx;
}
.error-message text {
font-size: 24rpx;
color: #ff6b6b;
}
/* 分割线 */
.divider {
width: 612rpx;
height: 4rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 100rpx;
margin: 28rpx 0 24rpx;
}
/* 下一步按钮 */
.next-btn-wrapper {
width: 100%;
display: flex;
justify-content: center;
padding: 64rpx 0;
}
.next-btn {
width: 270rpx;
height: 90rpx;
border-radius: 58rpx;
background: linear-gradient(90deg,
rgba(255, 222, 8, 0.28) 0%,
rgba(252, 100, 102, 0.58) 64%,
rgba(244, 88, 104, 0.58) 100%);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.96;
}
.next-btn-text {
font-size: 36rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
</style>