登陆时没有简历先上传一份简历

This commit is contained in:
2026-06-09 14:39:59 +08:00
parent 1c3ea6aa3c
commit 065553f53e
5 changed files with 549 additions and 190 deletions
+42
View File
@@ -1418,6 +1418,48 @@
}
}
// "还有其他问题"展开后的反馈表单
&__feedback {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.24rem;
}
// 文本输入框
&__textarea {
width: 100%;
min-height: 1.2rem;
padding: 0.2rem;
border-radius: 0.16rem;
border: none;
background: #f5f5f5;
font-size: 0.14rem;
line-height: 0.22rem;
color: #111;
resize: none;
outline: none;
font-family: inherit;
&::placeholder {
color: #bbb;
}
}
// 提交按钮
&__submit {
padding: 0.14rem 0.6rem;
border-radius: 9999px;
background: #111;
color: #fff;
font-size: 0.16rem;
line-height: 0.22rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: background 0.2s;
}
// 悬停态
&:hover {
.faq-item__icon {
+175
View File
@@ -445,3 +445,178 @@
text-decoration: underline;
}
}
/* ==================== 简历上传阶段 — 左侧职位卡片面板 ==================== */
.login-view__left--resume {
position: relative;
overflow: hidden;
padding: 0.4rem;
}
.login-view__resume-logo {
position: absolute;
top: 0.3rem;
left: 0.4rem;
display: flex;
align-items: center;
gap: 0.12rem;
z-index: 2;
}
.login-view__job-cards {
position: relative;
width: 100%;
height: 5.5rem;
z-index: 1;
}
.login-view__job-card {
position: absolute;
display: flex;
align-items: center;
gap: 0.1rem;
padding: 0.1rem 0.16rem;
background: #FFFFFF;
border-radius: 0.1rem;
box-shadow: 0 0.02rem 0.08rem rgba(0, 0, 0, 0.06);
white-space: nowrap;
}
.login-view__job-card-icon {
display: flex;
align-items: center;
justify-content: center;
width: 0.32rem;
height: 0.32rem;
border-radius: 0.06rem;
font-size: 0.13rem;
font-weight: 700;
flex-shrink: 0;
}
.login-view__job-card-info {
display: flex;
flex-direction: column;
gap: 0.02rem;
}
.login-view__job-card-title {
font-size: 0.13rem;
font-weight: 600;
color: #132034;
}
.login-view__job-card-company {
font-size: 0.11rem;
color: #94A3B8;
}
.login-view__job-card-salary {
font-size: 0.12rem;
font-weight: 600;
color: #E85635;
margin-left: 0.08rem;
}
.login-view__resume-slogan {
position: absolute;
bottom: 1.5rem;
left: 0.4rem;
z-index: 2;
h1 {
font-size: 0.32rem;
font-weight: 700;
color: #132034;
line-height: 0.48rem;
}
}
/* ==================== 简历上传阶段 — 右侧面板 ==================== */
.login-view__form-wrap--resume {
width: 4.6rem;
min-height: 4rem;
}
/* 上传区域 */
.login-view__upload-area {
width: 100%;
padding: 0.6rem 0.4rem;
border: 1px dashed #E2E8F0;
border-radius: 0.12rem;
background: #FFFFFF;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.12rem;
cursor: pointer;
transition: border-color 0.2s;
&:hover {
border-color: #12C7BE;
}
}
.login-view__upload-circle {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: #E8F8F7;
}
.login-view__upload-text {
font-size: 0.14rem;
color: #64748B;
}
.login-view__upload-hint {
font-size: 0.12rem;
color: #94A3B8;
}
.login-view__file-input {
display: none;
}
.login-view__upload-btn {
padding: 0.14rem 0.4rem;
background: linear-gradient(90deg, #12C7BE 0%, #06B6D4 100%);
border: none;
border-radius: 0.26rem;
font-size: 0.16rem;
font-weight: 600;
color: #FFFFFF;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.9;
}
}
/* 解析中状态 */
.login-view__parsing {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
width: 100%;
min-height: 3rem;
}
.login-view__parsing-title {
font-size: 0.32rem;
font-weight: 700;
color: #132034;
margin-bottom: 0.12rem;
}
.login-view__parsing-subtitle {
font-size: 0.15rem;
color: #64748B;
}
/* 失败状态标题红色 */
.login-view__title--error {
color: #12C7BE;
}
+33 -9
View File
@@ -348,11 +348,10 @@
:key="i"
class="faq-item"
:class="{ 'faq-item--open': faqOpen === i }"
@click="faqOpen = faqOpen === i ? -1 : i"
>
<div class="faq-item__header">
<div class="faq-item__header" @click="faqOpen === i ? null : (faqOpen = i)">
<span class="faq-item__question">{{ faq.q }}</span>
<div class="faq-item__icon">
<div class="faq-item__icon" @click.stop="faqOpen = faqOpen === i ? -1 : i">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<template v-if="faqOpen === i">
<line x1="7" y1="7" x2="17" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -366,7 +365,21 @@
</div>
</div>
<div class="faq-item__answer" v-show="faqOpen === i">
<p>{{ faq.a }}</p>
<!-- 最后一项"还有其他问题"显示文本输入框+提交按钮 -->
<template v-if="i === faqs.length - 1">
<div class="faq-item__feedback">
<textarea
v-model="faqFeedbackText"
class="faq-item__textarea"
placeholder="请输入你的问题"
rows="4"
></textarea>
<button class="faq-item__submit" @click.stop="submitFaqFeedback">提交</button>
</div>
</template>
<template v-else>
<p>{{ faq.a }}</p>
</template>
</div>
</div>
</div>
@@ -539,13 +552,24 @@ const testimonials = [
{ text: '“满分!特别是网申自动填写,不仅可以一键填写,还能快速定制简历内容,完美匹配岗位要求,毕设求职两不误,真心推荐。”', name: '张同学', school: '中国人民大学', avatar: studentAvatar2 },
]
/** 用户反馈输入内容 */
const faqFeedbackText = ref('')
/** 提交用户反馈 */
function submitFaqFeedback() {
if (!faqFeedbackText.value.trim()) return
// TODO: 对接后端反馈接口
faqFeedbackText.value = ''
faqOpen.value = -1
}
/** 常见问题列表 — q: 问题, a: 答案,点击可展开/收起 */
const faqs = [
{ q: '这个平台与其他求职网站有什么不同', a: 'Offer派专注于大学生校招场景,利用AI技术实现智能岗位匹配、一键自动网申、岗位定制简历和内推人脉直通,让求职效率提升80%以上。' },
{ q: '平台会分享我的个人信息吗?', a: '绝对不会。我们严格遵守隐私保护法规,您的个人信息仅用于岗位匹配简历投递,不会分享给任何第三方。' },
{ q: '如何收费?', a: '基础功能完全免费,包括岗位浏览、AI匹配和求职助手。高级功能如一键批量投递、定制简历等提供会员订阅服务。' },
{ q: '平台的岗位来源是什么', a: '我们的岗位信息来自各大企业官方校招渠道、合作高校就业中心以及企业HR直接发布,确保信息真实可靠。' },
{ q: '支持哪些企业?', a: '目前已覆盖互联网、金融、制造、快消等行业的数千家企业,包括字节跳动、腾讯、阿里巴巴、华为等头部企业。' },
{ q: '这个平台与其他求职平台有什么不同?', a: '不同于BOSS直聘等传统平台的“手动式”求职,Offer派如同一位经验丰富的职业导师,通过AI能力搭建智能工具,自动处理简历优化、岗位匹配、申请投递等环节,大幅度提升你的求职效率。' },
{ q: '平台如何保障我的个人信息安全?', a: 'Offer派高度重视你的隐私安全。未经你明确同意,我们绝不会将你的个人信息提供给任何第三方。你的所有数据仅用于在平台内为你提供AI智能岗位匹配简历优化等个性化求职服务。' },
{ q: '平台可以免费使用吗?', a: 'Offer派为所有用户免费开放3天会员权益,帮你省心省力找工作。3天后,依然可以免费使用各项功能,但有使用次数限制。如果你需要不受限制地使用所有功能,更快更高效地获得Offer,建议您升级为付费会员,让求职效率翻倍。' },
{ q: '平台的岗位来源是什么?', a: 'Offer派的岗位来自上万家企业官方招聘网站,同时聚合了各大主流平台的最新职位——你不需要在多个网站之间来回切换,就能一站式搜罗全网高质量的工作机会。此外,我们会持续筛查每一则新发布的职位,及时剔除虚假信息,帮你避“坑”,为你提供真实靠谱的岗位信息。' },
{ q: '还有其他问题?', a: '' },
]
// ==================== 底部筛选区:行业/地区/岗位选择器 ====================
+298 -180
View File
@@ -1,154 +1,158 @@
<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">
<!-- ==================== 登录阶段step 1 & 2==================== -->
<template v-if="step === 1 || step === 2">
<!-- 左侧品牌面板 -->
<div class="login-view__left">
<div class="login-view__deco-circle login-view__deco-circle--lg"></div>
<div class="login-view__left-content">
<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>
<div class="login-view__deco-circle login-view__deco-circle--sm"></div>
</div>
<!-- 右侧表单面板 -->
<div class="login-view__right">
<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 }} <span class="fw600 ml5 cursor-po" @click="handleChangePhone">修改手机号</span></p>
</div>
<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" />
<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>
<p v-if="smsStatusMsg" class="login-view__sms-status" :class="{ 'login-view__sms-status--error': smsStatusIsError }">{{ smsStatusMsg }}</p>
<button class="login-view__status-btn" :disabled="isLoggingIn || countdown > 0" @click="handleResendCode">
<template v-if="isLoggingIn"><span class="login-view__spinner"></span><span>登录中</span></template>
<template v-else-if="countdown > 0">{{ countdown }} 秒后可继续发送</template>
<template v-else>再次发送验证码</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>
</template>
<!-- ==================== 简历上传阶段step 3==================== -->
<template v-if="step === 3">
<!-- 左侧品牌面板 职位卡片风格 -->
<div class="login-view__left login-view__left--resume">
<!-- 顶部 Logo -->
<div class="login-view__resume-logo">
<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>
<!-- 浮动职位卡片 -->
<div class="login-view__job-cards">
<div v-for="(job, idx) in jobCards" :key="idx" class="login-view__job-card" :style="job.style">
<span class="login-view__job-card-icon" :style="{ background: job.iconBg, color: job.iconColor }">{{ job.icon }}</span>
<div class="login-view__job-card-info">
<span class="login-view__job-card-title">{{ job.title }}</span>
<span class="login-view__job-card-company">{{ job.company }}</span>
</div>
<span class="login-view__job-card-salary">{{ job.salary }}</span>
</div>
</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 class="login-view__resume-slogan">
<h1>校招批量海投</h1>
<h1>年轻人有更多的面试机会</h1>
</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__right">
<div class="login-view__form-wrap login-view__form-wrap--resume">
<!-- 表单区域 -->
<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>
<!-- 状态1上传区域 -->
<template v-if="resumeState === 'upload'">
<div class="login-view__heading">
<h2 class="login-view__title">一切美好从简历开始~</h2>
<p class="login-view__subtitle">自动解析您的简历为你匹配最合适的岗位</p>
</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 }} <span class="fw600 ml5 cursor-po" @click="handleChangePhone">修改手机号</span></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 class="login-view__upload-area" @dragover.prevent @drop.prevent="handleDrop" @click="triggerFileInput">
<div class="login-view__upload-circle"></div>
<p class="login-view__upload-text">拖拽简历到这里或点击上传</p>
<p class="login-view__upload-hint">支持 PDF / DOC / DOCX不超过 20MB</p>
</div>
</div>
<!-- 验证码发送状态提示 -->
<p v-if="smsStatusMsg" class="login-view__sms-status" :class="{ 'login-view__sms-status--error': smsStatusIsError }">{{ smsStatusMsg }}</p>
<!-- 状态按钮 三种状态登录中 / 倒计时 / 再次发送 -->
<button
class="login-view__status-btn"
:disabled="isLoggingIn || countdown > 0"
@click="handleResendCode"
>
<!-- 登录中状态 -->
<template v-if="isLoggingIn">
<span class="login-view__spinner"></span>
<span>登录中</span>
</template>
<!-- 倒计时状态 -->
<template v-else-if="countdown > 0">
{{ countdown }} 秒后可继续发送
</template>
<!-- 可再次发送 -->
<template v-else>
再次发送验证码
</template>
</button>
</template>
<!-- 隐藏的文件选择器 -->
<input ref="fileInputRef" type="file" accept=".pdf,.doc,.docx" class="login-view__file-input" @change="handleFileChange" />
<!-- 上传按钮 -->
<button class="login-view__upload-btn" @click="triggerFileInput">+ 上传简历</button>
</template>
<!-- 状态2解析中 -->
<template v-if="resumeState === 'parsing'">
<div class="login-view__parsing">
<h2 class="login-view__parsing-title">{{ parsingText }}</h2>
<p class="login-view__parsing-subtitle">自动解析您的简历为你匹配最合适的岗位</p>
</div>
</template>
<!-- 状态3上传失败 -->
<template v-if="resumeState === 'failed'">
<div class="login-view__heading">
<h2 class="login-view__title login-view__title--error">上传失败</h2>
<p class="login-view__subtitle">您的简历解析失败了请重试一次吧~</p>
</div>
<button class="login-view__upload-btn" @click="handleRetryUpload">重新上传</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>
</template>
<!-- 协议预览弹窗 -->
<AgreementPreviewDialog v-model="showAgreementDialog" :code="agreementCode" />
@@ -156,19 +160,23 @@
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { ref, computed, nextTick, onBeforeUnmount } from 'vue'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'
import { sendSmsCode, smsLogin } from '@/api/auth'
import { fetchProfile, syncProfileFromResume } from '@/api/profile'
import { uploadResume } from '@/utils/aiRequest'
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
const store = useStore()
const router = useRouter()
const route = useRoute()
/** 当前步骤:1=输入手机号 2=输入验证码 */
// ==================== 页面步骤控制 ====================
/** 当前步骤:1=输入手机号 2=输入验证码 3=上传简历 */
const step = ref(1)
// ==================== 登录相关状态 ====================
/** 手机号 */
const phone = ref('')
/** 验证码6位值 */
@@ -186,14 +194,44 @@ const smsStatusMsg = ref('')
const smsStatusIsError = ref(false)
/** OTP 输入框引用 */
const otpInputRef = ref<HTMLInputElement | null>(null)
/** 协议弹窗状态 */
const showAgreementDialog = ref(false)
const agreementCode = ref('')
/** 登录成功后的跳转路径(缓存) */
let loginRedirect = '/'
/** 发送按钮是否可用 — 手机号有值且已勾选协议 */
/** 发送按钮是否可用 */
const canSend = computed(() => phone.value.length === 11 && agreedTerms.value)
// ==================== 简历上传相关状态 ====================
/** 简历上传子状态:upload=上传区域 parsing=解析中 failed=失败 */
const resumeState = ref<'upload' | 'parsing' | 'failed'>('upload')
/** 解析中显示的文字 */
const parsingText = ref('正在解析个人资料...')
/** 解析文字轮换定时器 */
let parsingTimer: ReturnType<typeof setInterval> | null = null
/** 文件选择器引用 */
const fileInputRef = ref<HTMLInputElement | null>(null)
/** 左侧职位卡片硬编码数据 */
const jobCards = [
{ icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '前端工程师', company: '网易', salary: '18-28K', style: 'top: 0.6rem; left: 0.4rem;' },
{ icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '运营专员', company: '阿里巴巴', salary: '15-20K', style: 'top: 0.6rem; right: 0.8rem;' },
{ icon: '百', iconBg: '#E8F0FF', iconColor: '#4A7FE5', title: '算法工程师', company: '百度', salary: '30-45K', style: 'top: 1.5rem; left: 0.2rem;' },
{ icon: '美', iconBg: '#FFF3E8', iconColor: '#E8953F', title: '商业分析实习生', company: '美团', salary: '15K', style: 'top: 1.8rem; left: 2.8rem;' },
{ icon: '小', iconBg: '#E8FFE8', iconColor: '#4CAF50', title: '硬件产品', company: '小米', salary: '18-24K', style: 'top: 2.5rem; left: 0.3rem;' },
{ icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '产品运营', company: '京东', salary: '18-25K', style: 'top: 2.8rem; right: 0.6rem;' },
{ icon: '字', iconBg: '#F0E8FF', iconColor: '#7C4DFF', title: '产品经理', company: '字节跳动', salary: '25-35K', style: 'top: 3.5rem; left: 1.2rem;' },
{ icon: '增', iconBg: '#E8FFF5', iconColor: '#12C7BE', title: '增长运营', company: '拼多多', salary: '20-28K', style: 'top: 3.5rem; right: 0.4rem;' },
{ icon: '滴', iconBg: '#FFF8E1', iconColor: '#FFC107', title: '后端开发', company: '滴滴', salary: '22-32K', style: 'top: 4.3rem; left: 0.5rem;' },
{ icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '数据分析师', company: '腾讯', salary: '20-30K', style: 'top: 4.6rem; right: 0.8rem;' },
]
/** 解析中文字轮换列表 */
const parsingSteps = ['正在解析个人资料...', '正在解析教育背景...', '正在解析工作经历...', '正在解析技能特长...', '简历解析成功,即将跳转']
// ==================== 登录逻辑 ====================
/** 打开协议预览 */
function openAgreement(code: string) {
agreementCode.value = code
@@ -214,10 +252,7 @@ function handleChangePhone() {
smsStatusMsg.value = ''
smsStatusIsError.value = false
isLoggingIn.value = false
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
}
/** 开始60秒倒计时 */
@@ -225,36 +260,23 @@ function startCountdown() {
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0 && countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
if (countdown.value <= 0 && countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
}, 1000)
}
/** 步骤一:发送验证码 */
async function handleSendCode() {
if (!phone.value) {
ElMessage.warning('请输入手机号')
return
}
if (!agreedTerms.value) {
ElMessage.warning('请先同意用户协议与隐私政策')
return
}
if (!phone.value) { ElMessage.warning('请输入手机号'); return }
if (!agreedTerms.value) { ElMessage.warning('请先同意用户协议与隐私政策'); return }
try {
const res = await sendSmsCode(phone.value)
if (res.code === '0') {
smsStatusMsg.value = '验证码发送成功,请输入'
smsStatusIsError.value = false
// 进入步骤二
step.value = 2
// 开始倒计时
startCountdown()
// 自动聚焦验证码输入框
nextTick(() => focusOtpInput())
} else if (res.msg && res.msg.includes('请勿重复发送')) {
// 之前发送过的验证码仍有效,直接进入步骤二让用户输入
smsStatusMsg.value = '验证码还在有效期内,请输入'
smsStatusIsError.value = false
step.value = 2
@@ -265,21 +287,16 @@ async function handleSendCode() {
smsStatusIsError.value = true
}
} catch (err: any) {
// HTTP 500 等异常,拦截器已弹提示,这里同步显示到状态文字
const msg = err?.response?.data?.msg || '验证码发送失败'
smsStatusMsg.value = msg
smsStatusIsError.value = true
}
}
/** OTP输入处理 — 限制只能数字,满6位自动登录 */
/** OTP输入处理 */
function handleOtpInput() {
// 过滤非数字字符
otpValue.value = otpValue.value.replace(/\D/g, '').slice(0, 6)
// 满6位自动触发登录
if (otpValue.value.length === 6) {
handleLogin()
}
if (otpValue.value.length === 6) { handleLogin() }
}
/** 步骤二:再次发送验证码 */
@@ -314,30 +331,37 @@ async function handleLogin() {
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
}
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
countdown.value = 0
// 同步登录状态
store.commit('SET_AUTHENTICATED', true)
// 跳转到登录前目标页面
const redirect = (route.query.redirect as string) || '/'
router.replace(redirect)
// 缓存跳转路径
loginRedirect = (route.query.redirect as string) || '/'
// 检测是否需要上传简历
try {
const profileRes = await fetchProfile()
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
// 无个人资料,进入上传简历步骤
step.value = 3
resumeState.value = 'upload'
isLoggingIn.value = false
return
}
} catch {
// 接口异常不阻塞,直接跳转
}
// 有个人资料,直接跳转
router.replace(loginRedirect)
} catch {
ElMessage.error('登录失败请核对验证码')
otpValue.value = ''
@@ -345,4 +369,98 @@ async function handleLogin() {
nextTick(() => focusOtpInput())
}
}
// ==================== 简历上传逻辑 ====================
/** 触发文件选择 */
function triggerFileInput() {
fileInputRef.value?.click()
}
/** 文件选择器 change 事件 */
function handleFileChange(e: Event) {
const input = e.target as HTMLInputElement
if (input.files && input.files[0]) {
startUpload(input.files[0])
}
}
/** 拖放上传 */
function handleDrop(e: DragEvent) {
const file = e.dataTransfer?.files[0]
if (file) { startUpload(file) }
}
/** 重新上传 */
function handleRetryUpload() {
resumeState.value = 'upload'
}
/** 开始上传并解析简历 */
async function startUpload(file: File) {
// 校验文件类型和大小
const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']
if (!validTypes.includes(file.type)) {
ElMessage.warning('请上传 PDF / DOC / DOCX 格式文件')
return
}
if (file.size > 20 * 1024 * 1024) {
ElMessage.warning('文件大小不能超过 20MB')
return
}
// 进入解析状态
resumeState.value = 'parsing'
startParsingAnimation()
try {
const res = await uploadResume(file)
if (res.code === 0 && res.data?.resumeId) {
// 上传成功,同步个人资料
await syncProfileFromResume(String(res.data.resumeId))
// 停止轮换动画,显示成功
stopParsingAnimation()
parsingText.value = '简历解析成功,即将跳转'
// 等1秒后跳转
setTimeout(() => {
router.replace(loginRedirect)
}, 1000)
} else {
// 解析失败
stopParsingAnimation()
resumeState.value = 'failed'
}
} catch {
stopParsingAnimation()
resumeState.value = 'failed'
}
}
/** 开始解析文字轮换动画 */
function startParsingAnimation() {
let index = 0
parsingText.value = parsingSteps[0]
parsingTimer = setInterval(() => {
index++
if (index < parsingSteps.length - 1) {
// 前4个每2秒切换
parsingText.value = parsingSteps[index]
} else {
// 到第4个(技能特长)后停止轮换,保持显示直到接口返回
parsingText.value = parsingSteps[parsingSteps.length - 2]
if (parsingTimer) { clearInterval(parsingTimer); parsingTimer = null }
}
}, 2000)
}
/** 停止解析文字轮换动画 */
function stopParsingAnimation() {
if (parsingTimer) { clearInterval(parsingTimer); parsingTimer = null }
}
/** 组件卸载时清理定时器 */
onBeforeUnmount(() => {
if (countdownTimer) { clearInterval(countdownTimer) }
if (parsingTimer) { clearInterval(parsingTimer) }
})
</script>
+1 -1
View File
@@ -10,7 +10,7 @@
@save="handleSaveEdit"
/>
<!-- 欢迎上传简历弹窗首次无个人资料时自动弹出 -->
<!-- 欢迎上传简历弹窗无个人资料时自动弹出 -->
<ResumeUploadDialog v-model="showWelcomeDialog" @confirm="handleWelcomeUpload" />
<!-- 页面标题 -->