topfans/frontend/pages/login/login.vue
2026-04-27 12:59:19 +08:00

710 lines
14 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-container">
<!-- 背景图片层 -->
<image class="background-image" src="/static/background/login-bg.png" mode="aspectFill"></image>
<!-- 女孩图片 -->
<image class="girl-image" src="/static/login/person-photo.png" mode="aspectFit"></image>
<!-- 粉色玩偶遮挡女孩脸的下半部分 -->
<image class="doll-image" src="/static/login/login-character.png" mode="aspectFit"></image>
<view class="background-overlay"></view>
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- TOPFANS 标题 -->
<view class="title-wrapper">
<text class="title-text">TOPFANS</text>
</view>
<!-- 表单区域居中部分 -->
<view class="form-container">
<view class="form-wrapper">
<!-- 手机号输入框 -->
<view class="input-wrapper">
<input
class="input-field"
type="number"
v-model="form.phone"
placeholder="请输入手机号"
maxlength="11"
placeholder-class="input-placeholder"
/>
</view>
<!-- 密码输入框 -->
<view class="input-wrapper password-wrapper">
<input
class="input-field"
:type="showPassword ? 'text' : 'password'"
v-model="form.password"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
<view class="eye-icon" @click="togglePassword">
<text class="eye-text">{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
<!-- 登录按钮 -->
<button class="btn-primary" @click="handleLogin">登录</button>
<!-- 注册按钮 -->
<view class="register-link" @click="goToRegister">
<text>还没有账号?去注册</text>
</view>
</view>
</view>
<!-- 底部服务条款 -->
<view class="footer-text">
<view class="agreement-wrapper">
<view class="agreement-checkbox" @click="toggleAgreement">
<view class="custom-checkbox" :class="{ 'checked': agreedToTerms }">
<view v-if="agreedToTerms" class="checkbox-inner"></view>
</view>
<text class="agreement-label">我已阅读并同意</text>
</view>
<text class="agreement-link" @click="showAgreementModal">《Topfans用户服务协议》</text>
</view>
</view>
</view>
<!-- 提示弹窗 -->
<view v-if="showTipDialog" class="tip-dialog-mask" @click="closeTipDialog">
<view class="tip-dialog" @click.stop>
<view class="tip-dialog-header">
<text class="tip-dialog-title">提示</text>
<text class="tip-dialog-close" @click="closeTipDialog">×</text>
</view>
<view class="tip-dialog-content">
<text class="tip-text">阅读并同意以下条款</text>
<text class="tip-agreement-link" @click="openAgreementFromTip">《Topfans用户服务协议》</text>
</view>
</view>
</view>
<!-- 协议全文弹窗 -->
<view v-if="showAgreementDialog" class="agreement-dialog-mask" @click="closeAgreementDialog">
<view class="agreement-dialog" @click.stop>
<view class="agreement-dialog-header">
<text class="agreement-dialog-title">Topfans用户服务协议</text>
<text class="agreement-dialog-close" @click="closeAgreementDialog">×</text>
</view>
<scroll-view class="agreement-dialog-content" scroll-y>
<text class="agreement-text">{{ agreementContent || '协议内容加载中...' }}</text>
</scroll-view>
<view class="agreement-dialog-footer">
<button class="agreement-confirm-btn" @click="agreeAndClose">我同意</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { useStore } from 'vuex';
import { validatePhone, validatePassword } from '@/utils/validator';
import { AGREEMENT_CONTENT } from '@/utils/agreement';
import { resetAllGuides } from '@/utils/guideConfig';
const store = useStore();
// 响应式数据
const form = ref({
phone: '',
password: ''
});
const showPassword = ref(false);
const errorMessage = ref('');
const agreedToTerms = ref(false);
const agreementContent = ref('');
const showAgreementDialog = ref(false);
const showTipDialog = ref(false);
// 获取页面参数(用于显示错误信息)
const getPageParams = () => {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage.options && currentPage.options.error) {
errorMessage.value = decodeURIComponent(currentPage.options.error)
}
}
}
// 页面加载时获取错误信息
onLoad(() => {
getPageParams()
})
// 获取协议内容
const getAgreementContent = () => {
return AGREEMENT_CONTENT;
};
// 切换密码显示/隐藏
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
// 跳转到注册页面
const goToRegister = () => {
// 清除新手引导相关数据
resetAllGuides();
uni.removeStorageSync('is_new_user');
uni.removeStorageSync('has_seen_welcome');
uni.navigateTo({
url: '/pages/register/register'
});
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreedToTerms.value = !agreedToTerms.value;
};
// 显示协议全文弹窗
const showAgreementModal = () => {
// 直接加载协议内容(已从模块导入)
if (!agreementContent.value) {
agreementContent.value = getAgreementContent();
}
// 显示自定义协议弹窗
showAgreementDialog.value = true;
};
// 关闭协议弹窗
const closeAgreementDialog = () => {
showAgreementDialog.value = false;
};
// 同意并关闭弹窗
const agreeAndClose = () => {
agreedToTerms.value = true;
showAgreementDialog.value = false;
};
// 打开提示弹窗
const openTipDialog = () => {
showTipDialog.value = true;
};
// 关闭提示弹窗
const closeTipDialog = () => {
showTipDialog.value = false;
};
// 从提示弹窗打开协议弹窗
const openAgreementFromTip = () => {
showTipDialog.value = false;
// 延迟打开协议弹窗,确保提示弹窗先关闭
setTimeout(() => {
showAgreementModal();
}, 300);
};
// 登录
const handleLogin = async () => {
// 验证是否勾选协议
if (!agreedToTerms.value) {
openTipDialog();
return;
}
// 验证表单
const phoneValidation = validatePhone(form.value.phone);
if (!phoneValidation.valid) {
errorMessage.value = phoneValidation.message;
return;
}
const passwordValidation = validatePassword(form.value.password);
if (!passwordValidation.valid) {
errorMessage.value = passwordValidation.message;
return;
}
errorMessage.value = '';
try {
// 显示加载提示
uni.showLoading({
title: '登录中...',
mask: true
});
await store.dispatch('user/login', {
mobile: form.value.phone,
password: form.value.password
});
uni.hideLoading();
// 登录成功,跳转首页
uni.reLaunch({
url: '/pages/square/square'
});
} catch (error) {
uni.hideLoading();
// 显示错误信息(从响应的 message 中获取)
errorMessage.value = error.message || '登录失败,请重试';
uni.showToast({
title: errorMessage.value,
icon: 'none',
duration: 2000
});
}
};
</script>
<style scoped>
.login-container {
position: relative;
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.girl-image {
position: absolute;
top: 55%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
width: 180%;
height: 180%;
z-index: 1;
object-fit: contain;
object-position: center center;
}
.doll-image {
position: absolute;
bottom: -70%;
left: 45%;
transform: translateX(-50%);
width: 150%;
height: 150%;
z-index: 2;
object-fit: contain;
object-position: center bottom;
}
.background-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 3;
}
.content-wrapper {
position: relative;
z-index: 10;
width: 100%;
flex: 1;
min-height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 60rpx 60rpx 60rpx;
box-sizing: border-box;
}
.title-wrapper {
width: 100%;
padding-top: 120rpx;
margin-bottom: 80rpx;
text-align: center;
position: relative;
flex-shrink: 0;
}
.form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600rpx;
}
.title-text {
font-size: 150rpx;
font-weight: 600;
letter-spacing: 12rpx;
background: linear-gradient(to right, #B52920 0%, #86D9E0 80%, #56C1FF 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
line-height: 1.2;
text-transform: uppercase;
font-family: 'TheMiladiatorRegular', sans-serif !important;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.15));
display: flex;
justify-content: center;
}
.form-wrapper {
width: 100%;
}
.input-wrapper {
position: relative;
width: 100%;
height: 100rpx;
margin-bottom: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50rpx;
display: flex;
align-items: center;
padding: 0 40rpx;
box-sizing: border-box;
}
.password-wrapper {
padding-right: 100rpx;
}
.input-field {
flex: 1;
height: 100%;
font-size: 32rpx;
color: #333333;
}
.input-placeholder {
color: #999999;
}
.eye-icon {
position: absolute;
right: 40rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.eye-text {
font-size: 40rpx;
}
.error-message {
width: 100%;
padding: 20rpx 0;
margin-bottom: 20rpx;
text-align: center;
}
.error-message text {
font-size: 28rpx;
color: #ff4444;
}
.btn-primary {
width: 100%;
height: 100rpx;
margin-bottom: 30rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 50rpx;
border: none;
font-size: 36rpx;
font-weight: bold;
color: #e6e6e6;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary::after {
border: none;
}
.register-link {
width: 100%;
margin-top: 40rpx;
text-align: center;
}
.register-link text {
font-size: 28rpx;
color: #e6e6e6;
text-decoration: underline;
}
.footer-text {
position: relative;
width: 100%;
margin-top: auto;
padding: 0rpx 0 80rpx 0;
text-align: center;
}
.agreement-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
padding: 0 40rpx;
}
.agreement-checkbox {
display: flex;
align-items: center;
margin-right: 10rpx;
}
/* 自定义圆形checkbox样式 */
.agreement-checkbox {
display: flex;
align-items: center;
margin-right: 10rpx;
cursor: pointer;
}
.custom-checkbox {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #e6e6e6;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
position: relative;
}
.custom-checkbox.checked {
border-color: #e6e6e6;
background-color: transparent;
}
.checkbox-inner {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #000000;
}
.agreement-label {
font-size: 24rpx;
color: #e6e6e6;
margin-left: 10rpx;
}
.agreement-link {
font-size: 24rpx;
color: #e6e6e6;
text-decoration: underline;
cursor: pointer;
}
/* 协议弹窗样式 */
.agreement-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.agreement-dialog {
width: 100%;
max-width: 700rpx;
max-height: 80vh;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.agreement-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.agreement-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.agreement-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.agreement-dialog-content {
flex: 1;
padding: 40rpx;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
}
.agreement-text {
font-size: 28rpx;
color: #333333;
line-height: 1.8;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
width: 100%;
box-sizing: border-box;
display: block;
}
.agreement-dialog-footer {
padding: 30rpx 40rpx;
border-top: 1rpx solid #e5e5e5;
}
.agreement-confirm-btn {
width: 100%;
height: 88rpx;
background: #000000;
color: #e6e6e6;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.agreement-confirm-btn::after {
border: none;
}
/* 提示弹窗样式 */
.tip-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.tip-dialog {
width: 100%;
max-width: 600rpx;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tip-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.tip-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.tip-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.tip-dialog-content {
padding: 40rpx;
text-align: center;
}
.tip-text {
font-size: 32rpx;
color: #333333;
line-height: 1.6;
display: block;
margin-bottom: 20rpx;
}
.tip-agreement-link {
font-size: 32rpx;
color: #007AFF;
text-decoration: underline;
cursor: pointer;
display: block;
}
</style>