517 lines
13 KiB
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>
|