Files
xfc_userfront/src/views/Login.vue
2026-01-22 16:23:33 +08:00

431 lines
12 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.

<script setup>
import { ref, computed, onUnmounted, nextTick } from 'vue'
import { showToast } from 'vant'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useRouter, useRoute } from 'vue-router'
import { mobileCodeLogin } from '@/api/user'
import useApiFetch from '@/composables/useApiFetch'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const phoneNumber = ref('')
const verificationCode = ref('')
const isCountingDown = ref(false)
const countdown = ref(60)
const isAgreed = ref(false)
let timer = null
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
})
const canLogin = computed(() => {
return isPhoneNumberValid.value && verificationCode.value.length === 6 && isAgreed.value
})
const hideRegister = computed(() => route.query.from === 'promotionInquire')
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return
}
const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login' })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg)
}
}
}
function startCountdown() {
isCountingDown.value = true
countdown.value = 60
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
} else {
clearInterval(timer)
isCountingDown.value = false
}
}, 1000)
}
async function handleLogin() {
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return
}
if (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" });
return
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议和隐私政策" });
return
}
try {
const { data, error } = await mobileCodeLogin({
mobile: phoneNumber.value,
code: verificationCode.value
})
if (data.value && !error.value) {
if (data.value.code === 200) {
// 保存token
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
// 获取用户信息和代理信息
await Promise.all([
userStore.fetchUserInfo(),
agentStore.fetchAgentStatus()
])
showToast({ message: "登录成功!" });
setTimeout(() => {
const redirect = route.query.redirect ? decodeURIComponent(route.query.redirect) : '/'
router.replace(redirect)
}, 500)
} else {
showToast(data.value.msg || "登录失败,请重试")
}
}
} catch (err) {
console.error('登录失败:', err)
showToast({ message: "登录失败,请重试" });
}
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
const onClickLeft = () => {
router.back()
}
const goToRegister = () => {
router.push('/register')
}
function toUserAgreement() {
router.push(`/userAgreement`)
}
function toPrivacyPolicy() {
router.push(`/privacyPolicy`)
}
</script>
<template>
<div class="login-layout ">
<van-nav-bar fixed placeholder title="登录" left-text="" left-arrow @click-left="onClickLeft" />
<div class="login px-4 relative z-10">
<div class="mb-8 pt-20 text-left">
<div class="flex flex-col items-center">
<img class="h-16 w-16 rounded-full shadow" src="@/assets/images/logo.png" alt="Logo" />
<div class="report-title-wrapper">
<h1 class="report-title">大数据风险报告</h1>
<div class="report-title-decoration"></div>
</div>
</div>
</div>
<!-- 登录表单 -->
<div class="login-form">
<!-- 手机号输入 -->
<div class="form-item">
<div class="form-label">手机号</div>
<input v-model="phoneNumber" class="form-input" type="tel" placeholder="请输入手机号" maxlength="11" />
</div>
<!-- 验证码输入 -->
<div class="form-item">
<div class="form-label">验证码</div>
<div class="verification-input-wrapper">
<input v-model="verificationCode" id="verificationCode" class="form-input verification-input"
placeholder="请输入验证码" maxlength="6" />
<button class="get-code-btn" :class="{ 'disabled': isCountingDown || !isPhoneNumberValid }"
@click="sendVerificationCode" :disabled="isCountingDown || !isPhoneNumberValid">
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</button>
</div>
</div>
<!-- 协议同意框 -->
<div class="agreement-wrapper">
<input type="checkbox" v-model="isAgreed" class="agreement-checkbox accent-primary"
id="agreement" />
<label for="agreement" class="agreement-text">
我已阅读并同意
<a class="agreement-link" @click="toUserAgreement">用户协议</a>
<a class="agreement-link" @click="toPrivacyPolicy">隐私政策</a>
</label>
</div>
<!-- 提示文字 -->
<div class="notice-text">
未注册手机号登录后将自动注册账号并且代表您已阅读并同意
</div>
<!-- 登录按钮 -->
<button class="login-btn" :class="{ 'disabled': !canLogin }" @click="handleLogin" :disabled="!canLogin">
登录
</button>
<!-- 注册按钮推广链接来源的登录不展示 -->
<button v-if="!hideRegister" class="register-btn" @click="goToRegister">
注册成为代理
</button>
</div>
</div>
</div>
</template>
<style scoped>
.login-layout {
background-image: url('@/assets/images/login_bg.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
position: relative;
overflow: hidden;
}
/* 登录表单 */
.login-form {
background-color: var(--color-bg-primary);
padding: 2rem;
margin-top: 0.5rem;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
border-radius: 8px;
}
/* 表单项 */
.form-item {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
border: none;
border-bottom: 1px solid var(--color-border-primary);
}
.form-label {
font-size: 0.9375rem;
color: var(--color-text-primary);
margin-bottom: 0;
margin-right: 1rem;
font-weight: 500;
min-width: 4rem;
flex-shrink: 0;
}
.form-input {
width: 100%;
padding: 0.875rem 0;
font-size: 0.9375rem;
color: var(--color-text-primary);
outline: none;
background-color: transparent;
}
.form-input::placeholder {
color: var(--color-text-tertiary);
}
.form-input:focus {
border-bottom-color: var(--color-text-primary);
}
/* 验证码输入 */
.verification-input-wrapper {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.verification-input {
flex: 1;
padding-right: 6rem;
}
.get-code-btn {
position: absolute;
right: 0;
background: none;
border: none;
color: var(--color-primary);
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
font-weight: 500;
}
.get-code-btn.disabled {
color: var(--color-gray-400);
cursor: not-allowed;
}
/* 注册按钮 */
.register-btn {
width: 100%;
padding: 0.875rem;
background-color: transparent;
color: var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
letter-spacing: 0.25rem;
margin-top: 1rem;
}
.register-btn:hover {
background-color: var(--color-primary);
color: var(--color-text-white);
}
/* 登录按钮 */
.login-btn {
width: 100%;
padding: 0.875rem;
background-color: var(--color-primary);
color: var(--color-text-white);
border: none;
border-radius: 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: opacity 0.3s;
letter-spacing: 0.25rem;
}
.login-btn:hover {
opacity: 0.9;
}
.login-btn.disabled {
background-color: var(--color-gray-300);
cursor: not-allowed;
}
/* 协议同意 */
.agreement-wrapper {
display: flex;
align-items: center;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.agreement-checkbox {
flex-shrink: 0;
margin-right: 0.5rem;
}
.agreement-text {
font-size: 0.75rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
.agreement-link {
color: var(--color-primary);
cursor: pointer;
text-decoration: none;
}
/* 提示文字 */
.notice-text {
font-size: 0.6875rem;
color: var(--color-text-tertiary);
line-height: 1.5;
margin-bottom: 1rem;
}
/* 大数据风险报告标题样式 */
.report-title-wrapper {
position: relative;
margin-top: 1.5rem;
padding: 0 1rem;
}
.report-title {
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #00f2fe 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
letter-spacing: 0.1em;
margin: 0;
padding: 0.5rem 0;
position: relative;
z-index: 1;
animation: gradientShift 3s ease infinite;
text-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.report-title-decoration {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, transparent, #667eea, #764ba2, #667eea, transparent);
border-radius: 2px;
animation: decorationPulse 2s ease-in-out infinite;
}
@keyframes gradientShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes decorationPulse {
0%, 100% {
opacity: 0.6;
transform: translateX(-50%) scaleX(1);
}
50% {
opacity: 1;
transform: translateX(-50%) scaleX(1.2);
}
}
</style>