Files
----/前端源码/uni-app/components/screens/Login.vue

662 lines
17 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.
<template>
<view class="login-screen">
<!-- 背景装饰 -->
<view class="login-bg">
<view class="login-bg-pattern"></view>
</view>
<!-- 主要内容 -->
<view class="login-content">
<!-- Logo/标题区域 -->
<view class="login-header">
<view class="login-logo">
<text class="login-logo-text">壹梵</text>
</view>
<text class="login-title">壹梵起名</text>
<text class="login-subtitle">传承千年文化 · 赋予美好寓意</text>
</view>
<!-- 登录/注册表单 -->
<view class="login-form-wrapper">
<!-- 标签切换 -->
<view class="login-tabs">
<view class="login-tab" :class="{ active: currentTab === 'login' }" @click="currentTab = 'login'">
<text class="login-tab-text">登录</text>
</view>
<view class="login-tab" :class="{ active: currentTab === 'register' }" @click="currentTab = 'register'">
<text class="login-tab-text">注册</text>
</view>
</view>
<!-- 登录表单 -->
<view v-if="currentTab === 'login'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="loginForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号" maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入密码" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-forgot" @click="currentTab = 'forgot'">
<text class="login-forgot-text">忘记密码</text>
</view>
<button class="login-btn login-btn-primary" :disabled="!canLogin || loading" @click="handleLogin">
<text class="login-btn-text">{{ loading ? '登录中...' : '登录' }}</text>
</button>
</view>
<!-- 注册表单 -->
<view v-if="currentTab === 'register'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="registerForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">验证码</text>
<view class="login-form-code">
<input v-model="registerForm.code" type="tel" class="login-form-input" placeholder="请输入验证码"
maxlength="6" />
<button class="login-code-btn" :disabled="!canSendCode || countdown > 0"
@click="handleSendCode('register')">
<text class="login-code-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</button>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input v-model="registerForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入密码6-20位" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">确认密码</text>
<input v-model="registerForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请再次输入密码" />
</view>
<button class="login-btn login-btn-primary" :disabled="loading" @click="handleRegister">
<text class="login-btn-text">{{ loading ? '注册中...' : '注册' }}</text>
</button>
</view>
<!-- 忘记密码表单 -->
<view v-if="currentTab === 'forgot'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="forgotForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">验证码</text>
<view class="login-form-code">
<input v-model="forgotForm.code" type="tel" class="login-form-input" placeholder="请输入验证码" maxlength="6" />
<button class="login-code-btn" :disabled="!canSendCodeForgot || countdown > 0"
@click="handleSendCode('forgot')">
<text class="login-code-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</button>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">新密码</text>
<input v-model="forgotForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入新密码6-20位" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">确认密码</text>
<input v-model="forgotForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请再次输入新密码" />
</view>
<button class="login-btn login-btn-primary" :disabled="!canResetPassword || loading"
@click="handleResetPassword">
<text class="login-btn-text">{{ loading ? '重置中...' : '重置密码' }}</text>
</button>
<view class="login-back" @click="currentTab = 'login'">
<text class="login-back-text">返回登录</text>
</view>
</view>
</view>
<!-- 协议 -->
<view class="login-agreement">
<text class="login-agreement-text">
登录即表示同意
<text class="login-agreement-link" @click="handleNavigateToAgreement">用户协议</text>
<text class="login-agreement-link" @click="handleNavigateToPrivacy">隐私政策</text>
</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { useRouter } from 'vue-router';
import { userApi } from '@/api';
import type { MobileLoginResponse, MobileRegisterResponse, ForgotPasswordResponse } from '@/api/types';
import { showToast } from '@/utils/uni-compat';
const router = useRouter();
const emit = defineEmits<{
success: [data: MobileLoginResponse | MobileRegisterResponse | ForgotPasswordResponse];
}>();
const loading = ref(false);
const currentTab = ref<'login' | 'register' | 'forgot'>('login');
const showPassword = ref(false);
const countdown = ref(0);
let countdownTimer: number | null = null;
// 登录表单
const loginForm = reactive({
mobile: '',
password: '',
});
// 注册表单
const registerForm = reactive({
mobile: '',
code: '',
password: '',
repassword: '',
});
// 忘记密码表单
const forgotForm = reactive({
mobile: '',
code: '',
password: '',
repassword: '',
});
// 验证手机号
const isValidMobile = (mobile: string) => {
return /^1[3-9]\d{9}$/.test(mobile);
};
// 验证密码
const isValidPassword = (password: string) => {
return password.length >= 6 && password.length <= 20;
};
// 是否可以登录(仅根据 loading 控制按钮,去掉其他前置校验)
const canLogin = computed(() => {
return true;
});
// 是否可以注册
const canRegister = computed(() => {
return (
isValidMobile(registerForm.mobile) &&
registerForm.code.length === 6 &&
isValidPassword(registerForm.password) &&
registerForm.password === registerForm.repassword
);
});
// 是否可以发送验证码(注册)
const canSendCode = computed(() => {
return isValidMobile(registerForm.mobile);
});
// 是否可以发送验证码(忘记密码)
const canSendCodeForgot = computed(() => {
return isValidMobile(forgotForm.mobile);
});
// 是否可以重置密码
const canResetPassword = computed(() => {
return (
isValidMobile(forgotForm.mobile) &&
forgotForm.code.length === 6 &&
isValidPassword(forgotForm.password) &&
forgotForm.password === forgotForm.repassword
);
});
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
if (countdownTimer) {
clearInterval(countdownTimer);
}
countdownTimer = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}
}, 1000);
};
// 发送验证码
const handleSendCode = async (type: 'register' | 'forgot') => {
const mobile = type === 'register' ? registerForm.mobile : forgotForm.mobile;
if (!isValidMobile(mobile)) {
showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
try {
await userApi.sendSmsCode(mobile);
showToast({ title: '验证码已发送', icon: 'success' });
startCountdown();
} catch (error: any) {
showToast({ title: error.msg || '发送失败,请重试', icon: 'none' });
}
};
// 登录
const handleLogin = async () => {
if (!canLogin.value || loading.value) return;
try {
loading.value = true;
const result = await userApi.mobileLogin({
mobile: loginForm.mobile,
password: loginForm.password,
});
showToast({ title: '登录成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '登录失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 注册
const handleRegister = async () => {
if (!canRegister.value || loading.value) return;
if (registerForm.password !== registerForm.repassword) {
showToast({ title: '两次密码输入不一致', icon: 'none' });
return;
}
try {
loading.value = true;
const result = await userApi.mobileRegister({
mobile: registerForm.mobile,
password: registerForm.password,
repassword: registerForm.repassword,
verification_code: registerForm.code,
});
showToast({ title: '注册成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '注册失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 重置密码
const handleResetPassword = async () => {
if (!canResetPassword.value || loading.value) return;
if (forgotForm.password !== forgotForm.repassword) {
showToast({ title: '两次密码输入不一致', icon: 'none' });
return;
}
try {
loading.value = true;
const result = await userApi.forgotPassword({
mobile: forgotForm.mobile,
password: forgotForm.password,
repassword: forgotForm.repassword,
verification_code: forgotForm.code,
});
showToast({ title: '密码重置成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '重置失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 导航到用户协议
const handleNavigateToAgreement = () => {
router.push('/user-agreement');
};
// 导航到隐私政策
const handleNavigateToPrivacy = () => {
router.push('/privacy-policy');
};
</script>
<style scoped>
.login-screen {
position: relative;
width: 100%;
min-height: 100vh;
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.login-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
overflow: hidden;
}
.login-bg-pattern {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 30%, rgba(139, 35, 35, 0.03) 0%, transparent 50%);
animation: rotate 60s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.login-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 30px;
width: 100%;
max-width: 500px;
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
}
.login-logo {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(139, 35, 35, 0.3);
}
.login-logo-text {
font-size: 32px;
font-weight: bold;
color: #fdfbf7;
font-family: SimSun, "Songti SC", serif;
}
.login-title {
font-size: 28px;
font-weight: 500;
color: #2c2c2c;
margin-bottom: 8px;
font-family: SimSun, "Songti SC", serif;
letter-spacing: 0.2em;
}
.login-subtitle {
font-size: 14px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
.login-form-wrapper {
width: 100%;
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #eaddcf;
}
.login-tabs {
display: flex;
margin-bottom: 30px;
border-bottom: 2px solid #eaddcf;
}
.login-tab {
flex: 1;
padding: 12px 0;
text-align: center;
cursor: pointer;
position: relative;
transition: all 0.3s;
}
.login-tab.active {
color: #8b2323;
}
.login-tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #8b2323;
}
.login-tab-text {
font-size: 16px;
font-weight: 500;
font-family: SimSun, "Songti SC", serif;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.login-form-item {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
.login-form-label {
font-size: 14px;
color: #2c2c2c;
font-weight: 500;
font-family: SimSun, "Songti SC", serif;
}
.login-form-input {
width: 100%;
height: 44px;
padding: 0 16px;
background: #fff;
border: 1px solid #dcd3c9;
border-radius: 8px;
font-size: 14px;
color: #2c2c2c;
font-family: SimSun, "Songti SC", serif;
box-sizing: border-box;
}
.login-form-input:focus {
border-color: #8b2323;
outline: none;
}
.login-form-eye {
position: absolute;
right: 16px;
bottom: 12px;
cursor: pointer;
font-size: 18px;
}
.login-form-code {
display: flex;
gap: 10px;
align-items: center;
}
.login-form-code .login-form-input {
flex: 1;
}
.login-code-btn {
flex-shrink: 0;
height: 44px;
padding: 0 16px;
background: #8b2323;
color: #fff;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
font-family: SimSun, "Songti SC", serif;
}
.login-code-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-code-text {
font-size: 13px;
}
.login-forgot {
text-align: right;
margin-top: -10px;
cursor: pointer;
}
.login-forgot-text {
font-size: 13px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.login-back {
text-align: center;
margin-top: 10px;
cursor: pointer;
}
.login-back-text {
font-size: 14px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.login-btn {
width: 100%;
height: 48px;
border-radius: 24px;
border: none;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-family: SimSun, "Songti SC", serif;
cursor: pointer;
margin-top: 10px;
}
.login-btn-primary {
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
color: #fff;
}
.login-btn-primary:active {
background: linear-gradient(135deg, #701c1c 0%, #8b2323 100%);
transform: scale(0.98);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn-text {
font-size: 16px;
}
.login-agreement {
margin-top: 30px;
text-align: center;
}
.login-agreement-text {
font-size: 12px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
.login-agreement-link {
color: #8b2323;
cursor: pointer;
text-decoration: underline;
}
.login-agreement-link:hover {
opacity: 0.8;
}
</style>