693 lines
14 KiB
Vue
693 lines
14 KiB
Vue
<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 { 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 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>
|
||
|