840 lines
18 KiB
Vue
840 lines
18 KiB
Vue
<script setup>
|
||
import { ref } from 'vue';
|
||
import { login, register, loginByEmail, sendCodeToEmail, registerByEmail } from './api';
|
||
import { useUserStore } from '@/store/user.js';
|
||
import { showLoadingToast, closeToast } from '@/common/common.js';
|
||
|
||
const email = ref(''); // 邮箱
|
||
const password = ref(''); // 密码
|
||
const checked = ref(true); // 同意条款,默认勾选
|
||
const isLoading = ref(false); // 加载状态
|
||
|
||
// 注册相关变量
|
||
const showRegister = ref(false); // 显示注册弹窗
|
||
const registerEmail = ref(''); // 注册邮箱
|
||
const registerPassword = ref(''); // 注册密码
|
||
const confirmPassword = ref(''); // 确认密码
|
||
const verificationCode = ref(''); // 验证码
|
||
const codeBtnText = ref('获取验证码');
|
||
const codeBtnDisabled = ref(false);
|
||
let countdownTimer = null;
|
||
|
||
const userStore = useUserStore();
|
||
|
||
// 邮箱格式验证
|
||
const validateEmail = (email) => {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return emailRegex.test(email);
|
||
};
|
||
|
||
// 登录函数
|
||
const onLogin = async() => {
|
||
if (!checked.value) {
|
||
uni.showToast({
|
||
title: '请先同意用户协议',
|
||
icon: 'none',
|
||
duration: 2000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!email.value) {
|
||
uni.showToast({
|
||
title: '请输入邮箱',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!password.value) {
|
||
uni.showToast({
|
||
title: '请输入密码',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 邮箱格式验证
|
||
if (!validateEmail(email.value)) {
|
||
uni.showToast({
|
||
title: '请输入正确的邮箱',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 密码长度验证
|
||
if (password.value.length < 6) {
|
||
uni.showToast({
|
||
title: '密码至少6位',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 防止重复提交
|
||
if (isLoading.value) return;
|
||
|
||
isLoading.value = true;
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: '登录中...',
|
||
mask: true,
|
||
});
|
||
|
||
const params = {
|
||
email: email.value,
|
||
password: password.value
|
||
};
|
||
|
||
const res = await loginByEmail(params);
|
||
|
||
if (res && res.code === 0) { // 修改为code===0判断
|
||
// 存储用户信息到全局状态
|
||
userStore.setUserInfo(res.data);
|
||
userStore.setToken(res.data.accessToken);
|
||
userStore.setLoginStatus(true);
|
||
|
||
uni.showToast({
|
||
title: '登录成功',
|
||
icon: 'success',
|
||
});
|
||
|
||
uni.switchTab({
|
||
url:'/pages/index/index'
|
||
})
|
||
// 延迟一点时间再关闭弹窗,让用户看到成功提示
|
||
setTimeout(() => {
|
||
// 触发父组件的登录成功事件
|
||
emit('success');
|
||
emit('close');
|
||
}, 1000);
|
||
} else {
|
||
uni.showToast({
|
||
title: res.msg || '登录失败',
|
||
icon: 'error',
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('登录错误:', error);
|
||
uni.showToast({
|
||
title: error.message || '登录失败,请稍后重试',
|
||
icon: 'error',
|
||
});
|
||
} finally {
|
||
uni.hideLoading();
|
||
isLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 同意条款切换
|
||
const onChange = (e) => {
|
||
checked.value = e.detail.value; // 更新checked值
|
||
};
|
||
|
||
// 显示用户协议
|
||
const showAgreement = () => {
|
||
uni.showModal({
|
||
title: '用户协议',
|
||
content: '欢迎使用我们的服务!\n\n本协议规定了您使用我们服务的条款和条件,请仔细阅读。\n\n1. 服务使用条款\n2. 隐私政策\n3. 账户安全\n4. 免责声明\n\n继续使用即表示您同意遵守这些条款。',
|
||
showCancel: true,
|
||
confirmText: '同意',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
checked.value = true;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 忘记密码
|
||
const forgetPassword = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/auth/forget-password'
|
||
});
|
||
};
|
||
|
||
// 显示注册弹窗
|
||
const showRegisterPopup = () => {
|
||
showRegister.value = true;
|
||
};
|
||
|
||
// 隐藏注册弹窗
|
||
const hideRegisterPopup = () => {
|
||
showRegister.value = false;
|
||
// 清空注册表单
|
||
registerEmail.value = '';
|
||
registerPassword.value = '';
|
||
confirmPassword.value = '';
|
||
verificationCode.value = '';
|
||
};
|
||
|
||
// 获取验证码
|
||
const getVerificationCode = async () => {
|
||
if (!registerEmail.value) {
|
||
uni.showToast({
|
||
title: '请输入邮箱',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!validateEmail(registerEmail.value)) {
|
||
uni.showToast({
|
||
title: '请输入正确的邮箱',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 发送验证码
|
||
try {
|
||
uni.showLoading({
|
||
title: '发送中...',
|
||
mask: true,
|
||
});
|
||
|
||
const params = {
|
||
email: registerEmail.value,
|
||
register: true
|
||
};
|
||
|
||
const res = await sendCodeToEmail(params);
|
||
|
||
if (res && res.code === 0) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '验证码已发送',
|
||
icon: 'success',
|
||
});
|
||
|
||
// 开始倒计时
|
||
let timeLeft = 60;
|
||
codeBtnDisabled.value = true;
|
||
codeBtnText.value = `${timeLeft}秒`;
|
||
|
||
countdownTimer = setInterval(() => {
|
||
timeLeft--;
|
||
codeBtnText.value = `${timeLeft}秒`;
|
||
|
||
if (timeLeft <= 0) {
|
||
clearInterval(countdownTimer);
|
||
codeBtnText.value = '重新获取';
|
||
codeBtnDisabled.value = false;
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: res.msg || '发送失败',
|
||
icon: 'error',
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '发送失败,请重试',
|
||
icon: 'error',
|
||
});
|
||
}
|
||
};
|
||
|
||
// 注册提交
|
||
const onRegister = async () => {
|
||
if (!registerEmail.value) {
|
||
uni.showToast({
|
||
title: '请输入邮箱',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!validateEmail(registerEmail.value)) {
|
||
uni.showToast({
|
||
title: '请输入正确的邮箱',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!verificationCode.value) {
|
||
uni.showToast({
|
||
title: '请输入验证码',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!registerPassword.value) {
|
||
uni.showToast({
|
||
title: '请输入密码',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (registerPassword.value !== confirmPassword.value) {
|
||
uni.showToast({
|
||
title: '两次输入密码不一致',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (registerPassword.value.length < 6) {
|
||
uni.showToast({
|
||
title: '密码至少6位',
|
||
icon: 'none',
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: '注册中...',
|
||
mask: true,
|
||
});
|
||
|
||
// 调用注册API
|
||
const registerParams = {
|
||
email: registerEmail.value,
|
||
verificationCode: verificationCode.value,
|
||
password: registerPassword.value,
|
||
confirmPassword: confirmPassword.value
|
||
};
|
||
|
||
const res = await registerByEmail(registerParams);
|
||
|
||
if (res && res.code === 0) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '注册成功',
|
||
icon: 'success',
|
||
});
|
||
|
||
// 注册成功后直接调用登录接口
|
||
setTimeout(async () => {
|
||
try {
|
||
const loginRes = await loginByEmail({
|
||
email: registerEmail.value,
|
||
password: registerPassword.value
|
||
});
|
||
|
||
if (loginRes && loginRes.code === 0) {
|
||
// 存储用户信息到全局状态
|
||
userStore.setUserInfo(loginRes.data);
|
||
userStore.setToken(loginRes.data.accessToken);
|
||
userStore.setLoginStatus(true);
|
||
|
||
uni.showToast({
|
||
title: '登录成功',
|
||
icon: 'success',
|
||
});
|
||
|
||
// 关闭注册弹窗和登录弹窗
|
||
setTimeout(() => {
|
||
emit('success');
|
||
emit('close');
|
||
hideRegisterPopup();
|
||
}, 1000);
|
||
} else {
|
||
uni.showToast({
|
||
title: loginRes.msg || '自动登录失败',
|
||
icon: 'none',
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('自动登录失败:', error);
|
||
uni.showToast({
|
||
title: '自动登录失败,请手动登录',
|
||
icon: 'none',
|
||
});
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: res.msg || '注册失败',
|
||
icon: 'error',
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '注册失败,请稍后重试',
|
||
icon: 'error',
|
||
});
|
||
}
|
||
};
|
||
|
||
// 清空输入框
|
||
const clearInput = (type) => {
|
||
if (type === 'email') {
|
||
email.value = '';
|
||
} else if (type === 'password') {
|
||
password.value = '';
|
||
} else if (type === 'registerEmail') {
|
||
registerEmail.value = '';
|
||
} else if (type === 'registerPassword') {
|
||
registerPassword.value = '';
|
||
} else if (type === 'confirmPassword') {
|
||
confirmPassword.value = '';
|
||
} else if (type === 'verificationCode') {
|
||
verificationCode.value = '';
|
||
}
|
||
};
|
||
|
||
// 监听输入变化,清除错误状态
|
||
const onInputChange = (type) => {
|
||
if (type === 'email') {
|
||
// 移除邮箱错误状态
|
||
} else if (type === 'password') {
|
||
// 移除密码错误状态
|
||
}
|
||
};
|
||
|
||
// 暴露事件给父组件
|
||
const emit = defineEmits(['close', 'success']);
|
||
</script>
|
||
|
||
<template>
|
||
<view class="login-container">
|
||
<!-- 标题 -->
|
||
<view class="login-header">
|
||
<text class="title">欢迎登录</text>
|
||
<text class="subtitle">请使用您的账号信息登录</text>
|
||
</view>
|
||
|
||
<!-- 邮箱输入 -->
|
||
<view class="input-group">
|
||
<view class="label">邮箱</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="input-field"
|
||
type="text"
|
||
placeholder="请输入邮箱"
|
||
v-model="email"
|
||
@input="onInputChange('email')"
|
||
/>
|
||
<view
|
||
v-if="email"
|
||
class="clear-btn"
|
||
@tap="clearInput('email')"
|
||
>
|
||
×
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 密码输入 -->
|
||
<view class="input-group">
|
||
<view class="label">密码</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="input-field"
|
||
type="password"
|
||
placeholder="请输入密码"
|
||
v-model="password"
|
||
@input="onInputChange('password')"
|
||
/>
|
||
<view
|
||
v-if="password"
|
||
class="clear-btn"
|
||
@tap="clearInput('password')"
|
||
>
|
||
×
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 忘记密码链接 -->
|
||
<view class="links-section">
|
||
<text class="link" @tap="forgetPassword">忘记密码?</text>
|
||
</view>
|
||
|
||
<!-- 登录按钮 -->
|
||
<button
|
||
class="login-btn"
|
||
:class="{ 'btn-loading': isLoading }"
|
||
@tap="onLogin"
|
||
:disabled="isLoading"
|
||
>
|
||
<text v-if="!isLoading">登录</text>
|
||
<view v-else class="loading-spinner"></view>
|
||
</button>
|
||
|
||
<!-- 注册链接 -->
|
||
<view class="register-section">
|
||
<text>还没有账号?</text>
|
||
<text class="register-link" @tap="showRegisterPopup">立即注册</text>
|
||
</view>
|
||
|
||
<!-- 同意条款 -->
|
||
<view class="agreement-section">
|
||
<label class="agreement-label">
|
||
<checkbox
|
||
class="checkbox"
|
||
:checked="checked"
|
||
@change="onChange"
|
||
/>
|
||
我已阅读并同意
|
||
<text class="agreement-link" @tap="showAgreement">《用户协议》</text>
|
||
和
|
||
<text class="agreement-link" @tap="showAgreement">《隐私政策》</text>
|
||
</label>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 注册弹窗 -->
|
||
<u-popup
|
||
v-model:show="showRegister"
|
||
mode="center"
|
||
:border-radius="20"
|
||
:safe-area-inset-bottom="false"
|
||
width="80%"
|
||
:closeable="true"
|
||
@close="hideRegisterPopup"
|
||
>
|
||
<view class="register-popup">
|
||
<view class="register-header">
|
||
<text class="register-title">用户注册</text>
|
||
<text class="register-subtitle">请填写注册信息</text>
|
||
</view>
|
||
|
||
<!-- 注册邮箱输入 -->
|
||
<view class="input-group">
|
||
<view class="label">邮箱</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="input-field"
|
||
type="text"
|
||
placeholder="请输入邮箱"
|
||
v-model="registerEmail"
|
||
/>
|
||
<view
|
||
v-if="registerEmail"
|
||
class="clear-btn"
|
||
@tap="clearInput('registerEmail')"
|
||
>
|
||
×
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 验证码输入 -->
|
||
<view class="input-group">
|
||
<view class="label">验证码</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="input-field"
|
||
type="text"
|
||
placeholder="请输入验证码"
|
||
v-model="verificationCode"
|
||
/>
|
||
<view
|
||
v-if="verificationCode"
|
||
class="clear-btn"
|
||
@tap="clearInput('verificationCode')"
|
||
>
|
||
×
|
||
</view>
|
||
</view>
|
||
<button
|
||
class="code-btn"
|
||
:disabled="codeBtnDisabled"
|
||
@tap="getVerificationCode"
|
||
>
|
||
{{ codeBtnText }}
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 注册密码输入 -->
|
||
<view class="input-group">
|
||
<view class="label">密码</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="input-field"
|
||
type="password"
|
||
placeholder="请输入密码"
|
||
v-model="registerPassword"
|
||
/>
|
||
<view
|
||
v-if="registerPassword"
|
||
class="clear-btn"
|
||
@tap="clearInput('registerPassword')"
|
||
>
|
||
×
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 确认密码输入 -->
|
||
<view class="input-group">
|
||
<view class="label">确认密码</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="input-field"
|
||
type="password"
|
||
placeholder="请再次输入密码"
|
||
v-model="confirmPassword"
|
||
/>
|
||
<view
|
||
v-if="confirmPassword"
|
||
class="clear-btn"
|
||
@tap="clearInput('confirmPassword')"
|
||
>
|
||
×
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 注册按钮 -->
|
||
<button
|
||
class="register-submit-btn"
|
||
@tap="onRegister"
|
||
>
|
||
注册
|
||
</button>
|
||
|
||
<!-- 同意条款 -->
|
||
<view class="agreement-section">
|
||
<label class="agreement-label">
|
||
<checkbox
|
||
class="checkbox"
|
||
:checked="checked"
|
||
@change="onChange"
|
||
/>
|
||
我已阅读并同意
|
||
<text class="agreement-link" @tap="showAgreement">《用户协议》</text>
|
||
和
|
||
<text class="agreement-link" @tap="showAgreement">《隐私政策》</text>
|
||
</label>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.login-container {
|
||
padding: 40rpx;
|
||
min-height: 600rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.login-header {
|
||
margin-bottom: 50rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.title {
|
||
display: block;
|
||
font-size: 40rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.input-group {
|
||
margin-bottom: 30rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
padding-left: 20rpx;
|
||
}
|
||
|
||
.input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 88rpx;
|
||
background-color: #f8f8f8;
|
||
border-radius: 44rpx;
|
||
padding: 0 30rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.input-field {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.clear-btn {
|
||
position: absolute;
|
||
right: 30rpx;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #ccc;
|
||
border-radius: 50%;
|
||
color: white;
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.links-section {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.link {
|
||
font-size: 26rpx;
|
||
color: #00a88e;
|
||
}
|
||
|
||
.login-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: linear-gradient(90deg, #00a88e, #00c4a2);
|
||
border-radius: 44rpx;
|
||
color: white;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 20rpx;
|
||
border: none;
|
||
transition: opacity 0.3s;
|
||
|
||
&.btn-loading {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 30rpx;
|
||
height: 30rpx;
|
||
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
||
border-top: 3rpx solid white;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.register-section {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin: 30rpx 0;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.register-link {
|
||
color: #00a88e;
|
||
margin-left: 10rpx;
|
||
}
|
||
|
||
.agreement-section {
|
||
margin-top: auto;
|
||
text-align: center;
|
||
}
|
||
|
||
.agreement-label {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.checkbox {
|
||
transform: scale(0.7);
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.agreement-link {
|
||
color: #00a88e;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.register-popup {
|
||
padding: 40rpx;
|
||
min-height: 600rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.register-header {
|
||
margin-bottom: 50rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.register-title {
|
||
display: block;
|
||
font-size: 40rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.register-subtitle {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.code-btn {
|
||
position: absolute;
|
||
right: 10rpx;
|
||
top: 70%;
|
||
transform: translateY(-50%);
|
||
height: 60rpx;
|
||
padding: 0 20rpx;
|
||
background: linear-gradient(90deg, #00a88e, #00c4a2);
|
||
color: white;
|
||
font-size: 24rpx;
|
||
border-radius: 30rpx;
|
||
border: none;
|
||
z-index: 10;
|
||
|
||
&:disabled {
|
||
background: #ccc;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.register-submit-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: linear-gradient(90deg, #00a88e, #00c4a2);
|
||
border-radius: 44rpx;
|
||
color: white;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 20rpx;
|
||
border: none;
|
||
transition: opacity 0.3s;
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
</style>
|