Files
sweekar-app/pages/login/index.vue
T

840 lines
18 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 } 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>