upload project source code
This commit is contained in:
661
前端源码/uni-app/components/screens/Login.vue
Normal file
661
前端源码/uni-app/components/screens/Login.vue
Normal file
@@ -0,0 +1,661 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user