Files
cpzs-frontend-new/src/views/Login.vue

948 lines
23 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>
<div class="login-page-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">
<h1 class="main-title">用户登录</h1>
<p class="subtitle">您的专属彩票数据助理</p>
</div>
</div>
<!-- 登录表单 -->
<el-card class="login-form-container" shadow="never">
<!-- 被踢出提示 -->
<el-alert
v-if="showKickedOutAlert"
title="账号已在其他设备登录"
type="warning"
description="为保障账号安全,您的账号已在其他设备登录,当前会话已失效。请重新登录。"
show-icon
:closable="true"
@close="showKickedOutAlert = false"
style="margin-bottom: 20px"
/>
<div class="login-tabs">
<div
class="login-tab"
:class="{ active: loginType === 'account' }"
@click="switchLoginType('account')"
>
账号登录
</div>
<div
class="login-tab"
:class="{ active: loginType === 'phone' }"
@click="switchLoginType('phone')"
>
手机号登录
</div>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<!-- 账号登录表单 -->
<div v-if="loginType === 'account'">
<!-- 账号 -->
<div class="form-group">
<el-input
v-model="formData.username"
placeholder="请输入账号"
:error="errors.username"
prefix-icon="User"
size="large"
clearable
/>
<div v-if="errors.username" class="error-text">{{ errors.username }}</div>
</div>
<!-- 密码 -->
<div class="form-group">
<el-input
v-model="formData.password"
placeholder="请输入密码"
:error="errors.password"
prefix-icon="Lock"
size="large"
:type="showPassword ? 'text' : 'password'"
:show-password="true"
autocomplete="new-password"
/>
<div v-if="errors.password" class="error-text">{{ errors.password }}</div>
</div>
<!-- 记住密码 -->
<div class="form-options">
<el-checkbox v-model="formData.remember" label="记住密码" />
<router-link to="/reset-password" class="forgot-password-link">忘记密码?</router-link>
</div>
</div>
<!-- 手机号登录表单 -->
<div v-else>
<!-- 手机号和发送验证码按钮 -->
<div class="form-group">
<div class="phone-code-row">
<el-input
v-model="formData.phone"
type="tel"
placeholder="请输入手机号"
prefix-icon="Iphone"
size="large"
maxlength="11"
@input="validatePhoneInput"
@blur="validatePhoneOnBlur"
clearable
class="phone-input"
/>
<el-button
type="primary"
:disabled="codeBtnDisabled"
@click="sendVerificationCode"
class="send-code-btn-inline"
size="large"
>
{{ codeButtonText }}
</el-button>
</div>
<div v-if="errors.phone" class="error-text">{{ errors.phone }}</div>
<div v-else-if="formData.phone && formData.phone.length > 0 && formData.phone.length < 11" class="tip-text">请输入11位手机号码</div>
</div>
<!-- 验证码 -->
<div class="form-group">
<el-input
v-model="formData.code"
placeholder="请输入验证码"
prefix-icon="Key"
size="large"
maxlength="6"
/>
<div v-if="errors.code" class="error-text">{{ errors.code }}</div>
</div>
</div>
<!-- 登录按钮 -->
<el-button
type="primary"
native-type="submit"
:loading="loading"
class="login-btn"
size="large"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
<!-- 注册链接 -->
<div class="register-link">
<span>还没有账号</span>
<router-link to="/register" class="link">立即注册</router-link>
</div>
</form>
</el-card>
</div>
</template>
<script>
import { userStore } from '../store/user'
import { lotteryApi } from '../api/index.js'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
export default {
name: 'Login',
setup() {
const toast = useToast()
const router = useRouter()
return { toast, router }
},
data() {
return {
loginType: 'account', // 默认使用账号登录方式
showPassword: false,
loading: false,
codeCountdown: 0,
timer: null,
showPhoneError: false,
phoneValid: false,
showKickedOutAlert: false, // 是否显示被踢出提示
formData: {
username: '',
password: '',
phone: '',
code: '',
remember: false
},
errors: {}
}
},
computed: {
codeButtonText() {
return this.codeCountdown > 0 ? `${this.codeCountdown}秒后重试` : '获取验证码';
},
codeBtnDisabled() {
return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone);
}
},
mounted() {
// 检查是否因为被踢出而跳转到登录页
if (userStore.isKickedOut) {
this.showKickedOutAlert = true
// 显示提示后重置状态
setTimeout(() => {
userStore.resetKickedOutStatus()
}, 500)
}
},
methods: {
// 切换登录方式
switchLoginType(type) {
this.loginType = type;
this.errors = {};
// 切换时重置相关表单数据
if (type === 'account') {
this.formData.phone = '';
this.formData.code = '';
} else {
this.formData.username = '';
this.formData.password = '';
}
},
// 手机号格式验证
isValidPhone(phone) {
return /^1[3-9]\d{9}$/.test(phone);
},
// 手机号输入时验证
validatePhoneInput() {
// 清除之前的错误
this.errors.phone = '';
this.showPhoneError = false;
this.phoneValid = false;
const phone = this.formData.phone;
// 如果输入不是数字,替换非数字字符
if (!/^\d*$/.test(phone)) {
this.formData.phone = phone.replace(/\D/g, '');
}
// 如果长度达到11位验证格式
if (phone.length === 11) {
if (this.isValidPhone(phone)) {
this.phoneValid = true;
} else {
this.errors.phone = '手机号格式不正确';
this.showPhoneError = true;
}
}
},
// 手机号失焦时验证
validatePhoneOnBlur() {
const phone = this.formData.phone;
if (phone && phone.length > 0) {
if (phone.length !== 11) {
this.errors.phone = '手机号应为11位数字';
this.showPhoneError = true;
this.phoneValid = false;
} else if (!this.isValidPhone(phone)) {
this.errors.phone = '请输入正确的手机号码';
this.showPhoneError = true;
this.phoneValid = false;
} else {
this.phoneValid = true;
}
}
},
// 发送验证码
async sendVerificationCode() {
if (!this.formData.phone) {
this.errors.phone = '请输入手机号';
this.showPhoneError = true;
this.phoneValid = false;
return;
}
if (!this.isValidPhone(this.formData.phone)) {
this.errors.phone = '请输入正确的手机号码';
this.showPhoneError = true;
this.phoneValid = false;
return;
}
this.phoneValid = true;
try {
// 开始倒计时 (先启动倒计时避免API延迟导致用户体验不佳)
this.codeCountdown = 60;
this.startCodeCountdown();
const response = await lotteryApi.sendSmsCode(this.formData.phone);
if (response.success) {
this.toast.success('验证码已发送,请注意查收');
} else {
// 如果发送失败,停止倒计时
this.codeCountdown = 0;
clearInterval(this.timer);
this.toast.error(response.message || '发送验证码失败,请稍后重试');
}
} catch (error) {
// 如果发送出错,停止倒计时
this.codeCountdown = 0;
clearInterval(this.timer);
console.error('发送验证码失败:', error);
this.toast.error('发送验证码失败,请稍后重试');
}
},
// 开始倒计时
startCodeCountdown() {
if (this.timer) {
clearInterval(this.timer);
}
this.timer = setInterval(() => {
if (this.codeCountdown > 0) {
this.codeCountdown--;
} else {
clearInterval(this.timer);
this.timer = null;
}
}, 1000);
},
// 表单验证
validateForm() {
this.errors = {};
if (this.loginType === 'account') {
// 账号登录验证
if (!this.formData.username) {
this.errors.username = '请输入账号';
}
if (!this.formData.password) {
this.errors.password = '请输入密码';
} else if (this.formData.password.length < 6) {
this.errors.password = '密码至少6位';
}
} else {
// 手机号登录验证
if (!this.formData.phone) {
this.errors.phone = '请输入手机号';
this.showPhoneError = true;
} else if (!this.isValidPhone(this.formData.phone)) {
this.errors.phone = '请输入正确的手机号码';
this.showPhoneError = true;
}
if (!this.formData.code) {
this.errors.code = '请输入验证码';
} else if (this.formData.code.length < 4 || this.formData.code.length > 6) {
this.errors.code = '验证码格式不正确';
}
}
return Object.keys(this.errors).length === 0;
},
// 处理登录
async handleLogin() {
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
let response;
if (this.loginType === 'account') {
// 账号密码登录
response = await lotteryApi.userLogin(this.formData.username, this.formData.password);
} else {
// 手机号验证码登录
response = await lotteryApi.userPhoneLogin(this.formData.phone, this.formData.code);
}
if (response.success === true) {
// 登录成功调用getLoginUser获取完整用户信息
try {
const userInfo = await userStore.fetchLoginUser();
if (userInfo) {
// 触发Coze SDK重新初始化事件
setTimeout(() => {
window.dispatchEvent(new CustomEvent('reinitializeCozeSDK'));
console.log('已触发Coze SDK重新初始化事件');
}, 200);
// 直接跳转到个人中心
setTimeout(() => {
this.router.push('/profile');
}, 300);
} else {
this.toast.error('获取用户信息失败,请重新登录');
}
} catch (error) {
console.error('获取用户信息失败:', error);
this.toast.error('获取用户信息失败,请重新登录');
}
} else {
// 登录失败
this.toast.error(response.message || '登录失败,请检查账号密码');
}
} catch (error) {
console.error('登录失败:', error);
if (error.response && error.response.data) {
this.toast.error(error.response.data.message || '登录失败,请检查账号密码');
} else {
this.toast.error('网络错误,请重试');
}
} finally {
this.loading = false;
}
}
},
// 组件销毁时清除定时器
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
</script>
<style scoped>
/* 登录页面容器 */
.login-page-container {
min-height: calc(100vh - 70px);
background: var(--color-bg-page, #f0f2f5);
padding: 20px 20px 8px 20px;
}
/* 表单入场动画 */
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
padding: 35px 20px 25px;
text-align: center;
position: relative;
margin-bottom: 15px;
border-radius: var(--radius-md, 12px);
box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3);
animation: slideUp 0.5s ease;
}
.page-title {
margin: 0;
text-align: center;
}
.main-title {
font-size: 32px;
margin: 0 auto 4px;
font-weight: 700;
color: white;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
letter-spacing: 1px;
text-align: center;
width: 100%;
}
.subtitle {
font-size: 16px;
margin: 0;
color: white;
opacity: 0.95;
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
text-align: center;
width: 100%;
font-weight: 400;
}
/* 桌面端样式 */
@media (min-width: 1024px) {
.page-header {
padding: 30px 20px 25px;
}
}
.login-form-container {
padding: 0;
background: white;
margin: 0 0 20px 0;
border-radius: var(--radius-lg, 16px);
box-shadow: var(--shadow-lg, 0 8px 30px rgba(0, 0, 0, 0.12));
overflow: hidden;
animation: slideUp 0.5s ease 0.1s both;
}
/* 登录方式切换标签 */
.login-tabs {
display: flex;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
position: relative;
}
.login-tab {
flex: 1;
text-align: center;
padding: 18px 0;
font-size: 15px;
color: #999;
cursor: pointer;
transition: color 0.25s ease, background 0.25s ease;
position: relative;
font-weight: 500;
user-select: none;
}
.login-tab::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2.5px;
background: var(--color-primary, #e53e3e);
border-radius: 2px;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.login-tab.active {
color: var(--color-primary, #e53e3e);
background: white;
font-weight: 600;
}
.login-tab.active::after {
width: 40px;
}
.login-tab:hover:not(.active) {
color: #666;
background: #f8f8f8;
}
.login-form {
background: white;
border-radius: 0;
padding: 28px 24px 20px;
box-shadow: none;
}
/* 表单组 */
.form-group {
margin-bottom: 18px;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
border: 2px solid var(--color-border, #e9ecef);
border-radius: 6px;
background: var(--color-bg-input, #f8f9fa);
transition: all var(--transition-base, 0.25s ease);
min-height: 56px;
overflow: hidden;
}
.input-wrapper:focus-within {
border-color: var(--color-primary, #e53e3e);
background: white;
box-shadow: 0 0 0 3px var(--color-primary-bg, rgba(229, 62, 62, 0.08));
}
input:focus {
outline: none;
box-shadow: none;
}
.input-wrapper.error {
border-color: var(--color-danger, #dc3545);
background: #fff5f5;
}
.input-wrapper.success {
border-color: var(--color-success, #4caf50);
background: #f8fff8;
}
.input-icon {
padding: 12px 25px;
color: #6c757d;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
}
.icon-img {
width: 22px;
height: 22px;
object-fit: contain;
}
.form-input {
flex: 1;
padding: 16px 8px;
border: none;
outline: none;
font-size: 16px;
background: transparent;
color: var(--color-text-primary, #212529);
box-shadow: none;
-webkit-appearance: none;
appearance: none;
}
.form-input:focus {
outline: none;
border: none;
box-shadow: none;
}
/* 控制浏览器自动填充的样式 */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset !important;
-webkit-text-fill-color: #212529 !important;
transition: background-color 5000s ease-in-out 0s;
border: none !important;
outline: none !important;
}
/* 手机号和验证码按钮同行布局 */
.phone-code-row {
display: flex;
gap: 12px;
align-items: stretch;
}
.phone-input {
flex: 1;
}
/* 内联发送验证码按钮样式 */
.send-code-btn-inline {
background: linear-gradient(135deg, var(--color-primary, #e53e3e), var(--color-primary-light, #ff6b6b));
border: none;
border-radius: var(--radius-md, 12px);
font-weight: 500;
transition: all var(--transition-base, 0.25s ease);
min-width: 120px;
flex-shrink: 0;
height: auto;
display: flex;
align-items: center;
justify-content: center;
}
.send-code-btn-inline:hover:not(.is-disabled) {
background: linear-gradient(135deg, var(--color-primary-dark, #d43030), #ff5a5a);
transform: translateY(-1px);
box-shadow: 0 4px 15px var(--color-primary-shadow, rgba(229, 62, 62, 0.3));
}
.send-code-btn-inline:active:not(.is-disabled) {
transform: translateY(0) scale(0.98);
}
.send-code-btn-inline.is-disabled {
background: #d9d9d9 !important;
border-color: #d9d9d9 !important;
color: #999 !important;
transform: none !important;
box-shadow: none !important;
}
/* 提示文本 */
.error-text {
color: var(--color-danger, #f56565);
font-size: 12px;
margin-top: 6px;
margin-left: 4px;
animation: slideUp 0.2s ease;
}
.tip-text {
color: var(--color-text-tertiary, #a0aec0);
font-size: 12px;
margin-top: 6px;
margin-left: 4px;
}
/* 隐藏浏览器自带的密码控件 */
input::-ms-reveal,
input::-ms-clear {
display: none;
}
input::-webkit-credentials-auto-fill-button {
visibility: hidden;
position: absolute;
right: 0;
}
.form-input::placeholder {
color: var(--color-text-placeholder, #cbd5e0);
}
.password-toggle {
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-icon-img {
width: 22px;
height: 22px;
object-fit: contain;
}
/* 表单选项 */
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
}
.checkbox-wrapper {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: #666;
}
.checkbox-wrapper input {
display: none;
}
.checkmark {
width: 16px;
height: 16px;
border: 1px solid #ddd;
border-radius: 3px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: transparent;
transition: all 0.2s;
}
.checkbox-wrapper input:checked + .checkmark {
background: var(--color-primary, #e53e3e);
border-color: var(--color-primary, #e53e3e);
color: white;
}
/* 忘记密码链接 */
.forgot-password-link {
color: var(--color-primary, #e53e3e);
text-decoration: none;
font-size: 14px;
transition: color var(--transition-fast, 0.15s ease);
}
.forgot-password-link:hover {
color: var(--color-primary-dark, #c53030);
text-decoration: underline;
}
/* 登录按钮 */
.login-btn {
width: 100%;
margin: 24px 0 24px 0;
padding: 14px;
font-size: 16px;
font-weight: 600;
height: 52px;
background: linear-gradient(135deg, var(--color-primary, #e53e3e), var(--color-primary-light, #ff6b6b));
border: none;
border-radius: var(--radius-md, 12px);
box-shadow: 0 4px 20px var(--color-primary-shadow, rgba(229, 62, 62, 0.25));
transition: all var(--transition-base, 0.25s ease);
letter-spacing: 1px;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(229, 62, 62, 0.4);
background: linear-gradient(135deg, var(--color-primary-dark, #d43030), #ff5a5a);
border: none;
}
.login-btn:active:not(:disabled) {
transform: translateY(0) scale(0.98);
box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3);
}
/* Element UI 组件自定义样式 */
:deep(.el-input__wrapper) {
padding: 6px 16px;
box-shadow: none !important;
background-color: var(--color-bg-input, #f7fafc);
border: 2px solid var(--color-border, #e2e8f0);
border-radius: var(--radius-md, 12px);
transition: all var(--transition-base, 0.25s ease);
}
:deep(.el-input__wrapper.is-focus) {
background-color: #fff;
border-color: var(--color-primary, #e53e3e);
box-shadow: 0 0 0 3px var(--color-primary-bg, rgba(229, 62, 62, 0.08)) !important;
}
:deep(.el-input__prefix) {
margin-right: 12px;
color: var(--color-text-tertiary, #a0aec0);
transition: color var(--transition-base, 0.25s ease);
}
:deep(.el-input.is-focus .el-input__prefix) {
color: var(--color-primary, #e53e3e);
}
:deep(.el-input__inner) {
height: 44px;
font-size: 15px;
color: var(--color-text-primary, #1a202c);
}
:deep(.el-checkbox__label) {
font-size: 14px;
color: var(--color-text-secondary, #4a5568);
}
:deep(.el-checkbox__inner) {
border-color: #ddd;
transition: all 0.2s ease;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: var(--color-primary, #e53e3e);
border-color: var(--color-primary, #e53e3e);
}
:deep(.el-button.is-disabled) {
background: #d9d9d9;
border-color: #d9d9d9;
}
/* 注册链接 */
.register-link {
text-align: center;
font-size: 14px;
color: var(--color-text-secondary, #4a5568);
}
.register-link .link {
color: var(--color-primary, #e53e3e);
text-decoration: none;
margin-left: 5px;
font-weight: 500;
transition: color var(--transition-fast, 0.15s ease);
}
.register-link .link:hover {
color: var(--color-primary-dark, #c53030);
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-page-container {
padding: 10px 10px 5px 10px;
}
.login-form {
padding: 22px 20px 18px;
}
}
@media (max-width: 480px) {
.login-page-container {
padding: 5px 5px 3px 5px;
}
.page-header {
padding: 30px 20px 25px;
margin-bottom: 12px;
}
.page-title {
margin: 0;
}
.main-title {
font-size: 28px;
margin-bottom: 3px;
letter-spacing: 0.5px;
}
.subtitle {
font-size: 14px;
}
.login-form {
padding: 28px 20px 20px;
}
.login-tab {
padding: 16px 0;
font-size: 14px;
}
:deep(.el-input__inner) {
height: 42px;
font-size: 14px;
}
.login-btn {
height: 48px;
font-size: 15px;
margin: 20px 0 20px 0;
}
.send-code-btn-inline {
font-size: 12px;
min-width: 90px;
padding: 0 10px;
}
.phone-code-row {
gap: 8px;
}
}
</style>