登陆时没有简历先上传一份简历
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+32
-8
@@ -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">
|
||||
<!-- 最后一项"还有其他问题"显示文本输入框+提交按钮 -->
|
||||
<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: '' },
|
||||
]
|
||||
|
||||
// ==================== 底部筛选区:行业/地区/岗位选择器 ====================
|
||||
|
||||
+245
-127
@@ -1,145 +1,78 @@
|
||||
<template>
|
||||
<!-- 登录页面 — 左右分栏布局 -->
|
||||
<!-- 登录页面 — 左右分栏布局,包含登录和简历上传流程 -->
|
||||
<div class="login-view">
|
||||
|
||||
<!-- ==================== 登录阶段(step 1 & 2)==================== -->
|
||||
<template v-if="step === 1 || step === 2">
|
||||
<!-- 左侧品牌面板 -->
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<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
|
||||
}"
|
||||
>
|
||||
<!-- 已输入的数字 -->
|
||||
<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 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"
|
||||
>
|
||||
<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>
|
||||
@@ -149,6 +82,77 @@
|
||||
</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>
|
||||
<!-- 标语 -->
|
||||
<div class="login-view__resume-slogan">
|
||||
<h1>校招批量海投</h1>
|
||||
<h1>年轻人有更多的面试机会</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧简历上传面板 -->
|
||||
<div class="login-view__right">
|
||||
<div class="login-view__form-wrap login-view__form-wrap--resume">
|
||||
|
||||
<!-- 状态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__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>
|
||||
<!-- 隐藏的文件选择器 -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@save="handleSaveEdit"
|
||||
/>
|
||||
|
||||
<!-- 欢迎上传简历弹窗(首次无个人资料时自动弹出) -->
|
||||
<!-- 欢迎上传简历弹窗(无个人资料时自动弹出) -->
|
||||
<ResumeUploadDialog v-model="showWelcomeDialog" @confirm="handleWelcomeUpload" />
|
||||
|
||||
<!-- 页面标题 -->
|
||||
|
||||
Reference in New Issue
Block a user