新增登陆页面,去掉之前的窗口登陆

This commit is contained in:
2026-06-04 17:41:07 +08:00
parent 2844d99ae1
commit 540d61b6a0
7 changed files with 747 additions and 82 deletions
+319
View File
@@ -0,0 +1,319 @@
<template>
<!-- 登录页面 左右分栏布局 -->
<div class="login-view">
<!-- 左侧品牌面板 -->
<div class="login-view__left">
<!-- 装饰圆圈 1 -->
<div class="login-view__deco-circle login-view__deco-circle--lg"></div>
<!-- 中心内容 -->
<div class="login-view__left-content">
<!-- Logo -->
<div class="login-view__logo-row">
<div class="login-view__logo-box login-view__logo-box--light">O</div>
<span class="login-view__logo-text login-view__logo-text--white">Offer派</span>
</div>
<!-- 标语 -->
<h1 class="login-view__slogan">在Offer派<br/>找到你的理想工作</h1>
<!-- 副标语 -->
<p class="login-view__sub-slogan">AI 驱动的求职平台为你匹配最合适的岗位</p>
<!-- 特性标签 -->
<div class="login-view__pills">
<div class="login-view__pill"> AI 智能岗位匹配</div>
<div class="login-view__pill"> 一键生成专属简历</div>
<div class="login-view__pill"> 模拟面试精准备考</div>
</div>
</div>
<!-- 装饰圆圈 2 -->
<div class="login-view__deco-circle login-view__deco-circle--sm"></div>
</div>
<!-- 右侧表单面板 -->
<div class="login-view__right">
<!-- 右侧 Logo -->
<div class="login-view__logo-row login-view__logo-row--right">
<div class="login-view__logo-box login-view__logo-box--green">O</div>
<span class="login-view__logo-text login-view__logo-text--dark">Offer派</span>
</div>
<!-- 表单区域 -->
<div class="login-view__form-wrap">
<!-- ========== 步骤一输入手机号发送验证码 ========== -->
<template v-if="step === 1">
<!-- 标题 -->
<div class="login-view__heading">
<h2 class="login-view__title">手机号登录/注册</h2>
<p class="login-view__subtitle">未注册用户验证后将自动注册并登录</p>
</div>
<!-- 手机号输入行 -->
<div class="login-view__phone-row">
<!-- 国家编码选择 -->
<div class="login-view__country-code">
<span class="login-view__flag">🇨🇳</span>
<span class="login-view__code-text">+86</span>
<span class="login-view__code-arrow"></span>
</div>
<!-- 分隔线 -->
<div class="login-view__separator"></div>
<!-- 手机号输入框 -->
<input
v-model="phone"
type="tel"
class="login-view__phone-input"
placeholder="请输入手机号码"
maxlength="11"
/>
</div>
<!-- 发送验证码按钮 -->
<button
class="login-view__send-btn"
:disabled="!canSend"
@click="handleSendCode"
>
发送验证码
</button>
</template>
<!-- ========== 步骤二输入验证码登录 ========== -->
<template v-if="step === 2">
<!-- 标题 -->
<div class="login-view__heading">
<h2 class="login-view__title">请输入验证码</h2>
<p class="login-view__subtitle">短信验证码已发送至 +86{{ phone }}</p>
</div>
<!-- 6位验证码输入框 -->
<div class="login-view__otp-wrap" @click="focusOtpInput">
<!-- 隐藏的真实输入框 -->
<input
ref="otpInputRef"
v-model="otpValue"
type="text"
inputmode="numeric"
class="login-view__otp-hidden-input"
maxlength="6"
:disabled="isLoggingIn"
@input="handleOtpInput"
/>
<!-- 6个显示方块 -->
<div
v-for="(_, idx) in 6"
:key="idx"
class="login-view__otp-box"
:class="{
'login-view__otp-box--active': idx === otpValue.length && !isLoggingIn,
'login-view__otp-box--filled': idx < otpValue.length
}"
>
<!-- 已输入的数字 -->
<span v-if="idx < otpValue.length" class="login-view__otp-digit">{{ otpValue[idx] }}</span>
<!-- 闪烁光标 当前待输入位 -->
<span v-else-if="idx === otpValue.length && !isLoggingIn" class="login-view__otp-cursor"></span>
</div>
</div>
<!-- 状态按钮非触发登录 -->
<button class="login-view__status-btn" disabled>
<!-- 登录中状态 -->
<template v-if="isLoggingIn">
<span class="login-view__spinner"></span>
<span>登录中</span>
</template>
<!-- 即将返回提示 -->
<template v-else-if="isReturning">
即将返回发送验证码
</template>
<!-- 倒计时状态 -->
<template v-else>
{{ countdown }} 秒后可继续发送
</template>
</button>
</template>
<!-- 协议勾选两个步骤通用 -->
<div class="login-view__agreement">
<div
class="login-view__checkbox"
:class="{ 'login-view__checkbox--checked': agreedTerms }"
@click="agreedTerms = !agreedTerms"
>
<span v-if="agreedTerms" class="login-view__check-icon"></span>
</div>
<span class="login-view__agreement-text">登录即表示同意</span>
<span class="login-view__agreement-link" @click="openAgreement('ae8065i3')">用户协议</span>
<span class="login-view__agreement-text"></span>
<span class="login-view__agreement-link" @click="openAgreement('hf8375i8')">隐私政策</span>
</div>
</div>
</div>
<!-- 协议预览弹窗 -->
<AgreementPreviewDialog v-model="showAgreementDialog" :code="agreementCode" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'
import { sendSmsCode, smsLogin } from '@/api/auth'
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
const store = useStore()
const router = useRouter()
const route = useRoute()
/** 当前步骤:1=输入手机号 2=输入验证码 */
const step = ref(1)
/** 手机号 */
const phone = ref('')
/** 验证码6位值 */
const otpValue = ref('')
/** 是否已勾选协议 */
const agreedTerms = ref(false)
/** 倒计时秒数 */
const countdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | null = null
/** 是否正在调用登录接口 */
const isLoggingIn = ref(false)
/** 是否处于"即将返回"提示阶段 */
const isReturning = ref(false)
/** 用户最后一次输入验证码的时间戳 */
let lastOtpInputTime = 0
/** OTP 输入框引用 */
const otpInputRef = ref<HTMLInputElement | null>(null)
/** 协议弹窗状态 */
const showAgreementDialog = ref(false)
const agreementCode = ref('')
/** 发送按钮是否可用 — 手机号有值且已勾选协议 */
const canSend = computed(() => phone.value.length === 11 && agreedTerms.value)
/** 打开协议预览 */
function openAgreement(code: string) {
agreementCode.value = code
showAgreementDialog.value = true
}
/** 聚焦OTP隐藏输入框 */
function focusOtpInput() {
otpInputRef.value?.focus()
}
/** 开始60秒倒计时 */
function startCountdown() {
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0 && countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
// 倒计时结束,进入"即将返回"提示阶段
triggerReturn()
}
}, 1000)
}
/** 倒计时结束后的返回逻辑 — 考虑用户最近操作 */
function triggerReturn() {
// 计算距离用户最后一次输入过了多久
const elapsed = Date.now() - lastOtpInputTime
// 如果用户5秒内还在输入,则等待剩余时间后再提示
const waitTime = Math.max(0, 5000 - elapsed)
// 先等用户操作冷却
setTimeout(() => {
// 显示"即将返回发送验证码"提示5秒
isReturning.value = true
setTimeout(() => {
isReturning.value = false
step.value = 1
otpValue.value = ''
}, 5000)
}, waitTime)
}
/** 步骤一:发送验证码 */
async function handleSendCode() {
if (!phone.value) {
ElMessage.warning('请输入手机号')
return
}
if (!agreedTerms.value) {
ElMessage.warning('请先同意用户协议与隐私政策')
return
}
try {
const res = await sendSmsCode(phone.value)
if (res.code === '0') {
ElMessage.success('验证码已发送')
// 进入步骤二
step.value = 2
// 开始倒计时
startCountdown()
// 自动聚焦验证码输入框
nextTick(() => focusOtpInput())
} else if (res.msg && res.msg.includes('请勿重复发送')) {
// 之前发送过的验证码仍有效,直接进入步骤二让用户输入
ElMessage.warning(res.msg)
step.value = 2
startCountdown()
nextTick(() => focusOtpInput())
} else {
ElMessage.error(res.msg || '验证码发送失败')
}
} catch {
// 错误已在 request 拦截器中统一处理
}
}
/** OTP输入处理 — 限制只能数字,满6位自动登录 */
function handleOtpInput() {
// 过滤非数字字符
otpValue.value = otpValue.value.replace(/\D/g, '').slice(0, 6)
// 记录用户最后输入时间
lastOtpInputTime = Date.now()
// 满6位自动触发登录
if (otpValue.value.length === 6) {
handleLogin()
}
}
/** 调用登录接口 */
async function handleLogin() {
isLoggingIn.value = true
try {
const res = await smsLogin(phone.value, otpValue.value)
if (res.code !== '0') {
ElMessage.error('登录失败请核对验证码')
// 清空已输入的验证码
otpValue.value = ''
isLoggingIn.value = false
// 重新聚焦输入框
nextTick(() => focusOtpInput())
return
}
// 登录成功
ElMessage.success(`欢迎回来,${res.data?.nick || ''}`)
// 清除倒计时
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
countdown.value = 0
// 同步登录状态
store.commit('SET_AUTHENTICATED', true)
// 跳转到登录前目标页面
const redirect = (route.query.redirect as string) || '/'
router.replace(redirect)
} catch {
ElMessage.error('登录失败请核对验证码')
otpValue.value = ''
isLoggingIn.value = false
nextTick(() => focusOtpInput())
}
}
</script>