新增登陆页面,去掉之前的窗口登陆
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user