全项目加载动效重做和简历经历区域的时间和编辑按钮同位置显示切换

This commit is contained in:
2026-06-30 17:09:09 +08:00
parent 0aebde7bd3
commit 8773c65b28
24 changed files with 610 additions and 489 deletions
+1
View File
@@ -38,6 +38,7 @@ declare module 'vue' {
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IndustrySelector: typeof import('./src/components/tools/IndustrySelector.vue')['default']
JobCategorySelector: typeof import('./src/components/tools/JobCategorySelector.vue')['default']
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -6,7 +6,7 @@
position: fixed;
inset: 0;
background: $overlay-bg;
z-index: 2000;
z-index: 2300;
display: flex;
align-items: center;
justify-content: center;
@@ -83,7 +83,7 @@
font-size: 0.13rem;
color: $text-dark;
background: $bg-white;
border: 1px solid $border-color;
border: none;
border-radius: 0.04rem;
outline: none;
transition: all 0.2s;
@@ -317,8 +317,7 @@
&__ai-result-card {
padding: 0.1rem 0.12rem;
background: $theme-color;
border: 1px solid rgba($accent, 0.2);
background: linear-gradient(to bottom,#F5FFFD,#ffffff);
border-radius: 0.06rem;
}
@@ -189,8 +189,8 @@
}
&__item-date {
font-size: 0.11rem;
color: $accent;
font-size: 0.13rem;
color: #64748B;
margin-bottom: 0.04rem;
}
@@ -328,12 +328,29 @@
// 条目hover时显示编辑按钮
&__section-item:hover &__item-edit-btn {
display: flex;
opacity: 1;
pointer-events: auto;
}
// 条目内编辑按钮
// hover条目时隐藏时间
&__section-item:hover &__item-date--toggle {
display: none;
}
// 条目内编辑按钮(默认隐藏不占位)
&__item-edit-btn {
margin-left: 0.08rem;
display: none;
padding-top: 0;
padding-bottom: 0;
}
// 时间切换样式(默认显示,靠右)
&__item-date--toggle {
margin-left: auto;
font-size: 0.13rem;
color: #64748B;
flex-shrink: 0;
}
}
@@ -1,6 +1,6 @@
@use '../variables' as *;
// ==================== 邀请注册送会员弹窗 ====================
// ==================== 邀请注册送会员弹窗(重设计版) ====================
.settings-invite-overlay {
position: fixed;
top: 0;
@@ -18,178 +18,211 @@
.settings-invite-dialog {
position: relative;
width: 4.2rem;
max-height: 85vh;
height: 5.0rem;
background: $bg-white;
border-radius: 0.12rem;
padding: 0.32rem 0.28rem;
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15);
overflow-y: auto;
border-radius: 0.26rem;
box-shadow: 0 0.2rem 0.44rem rgba(0, 0, 0, 0.14);
overflow: hidden;
display: flex;
flex-direction: column;
// 顶部渐变区域
&__header {
width: 100%;
height: 1.04rem;
background: linear-gradient(90deg, #12C7BE 0%, #06B6D4 100%);
border-radius: 0.26rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
// 皇冠图标
&__crown {
font-size: 0.32rem;
line-height: 0.39rem;
color: $bg-white;
font-weight: 700;
}
// 关闭按钮
&__close {
position: absolute;
top: 0.14rem;
right: 0.18rem;
font-size: 0.18rem;
color: $text-light;
top: 0.24rem;
right: 0.27rem;
font-size: 0.22rem;
line-height: 0.27rem;
color: #8EA0B8;
cursor: pointer;
transition: color 0.2s;
z-index: 1;
&:hover {
color: $text-dark;
}
}
// 内容区域
&__body {
flex: 1;
padding: 0.22rem 0.28rem 0.2rem;
display: flex;
flex-direction: column;
}
// 标题
&__title {
font-size: 0.18rem;
font-size: 0.21rem;
font-weight: 700;
color: $text-dark;
color: #132034;
text-align: center;
margin: 0 0 0.05rem 0;
line-height: 0.25rem;
}
// 副标题
&__subtitle {
font-size: 0.13rem;
font-weight: 400;
color: #64748B;
text-align: center;
margin: 0 0 0.2rem 0;
line-height: 0.16rem;
}
// 邀请链接区域
&__link-box {
background: $bg-main;
border-radius: 0.08rem;
padding: 0.14rem 0.16rem;
margin-bottom: 0.14rem;
display: flex;
align-items: center;
width: 3.64rem;
height: 0.44rem;
background: #F3FFFD;
border-radius: 0.22rem;
padding: 0 0.06rem 0 0.16rem;
margin: 0 auto 0.2rem;
box-sizing: border-box;
}
&__link-text {
font-size: 0.12rem;
color: $text-dark;
line-height: 1.7;
margin: 0;
word-break: break-all;
flex: 1;
font-size: 0.11rem;
line-height: 0.13rem;
color: #334155;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 复制链接按钮
&__copy-btn {
width: 100%;
height: 0.4rem;
width: 1.0rem;
height: 0.32rem;
border: none;
border-radius: 0.2rem;
background: $text-dark;
border-radius: 0.16rem;
background: #0F172A;
color: $bg-white;
font-size: 0.14rem;
font-size: 0.13rem;
font-weight: 600;
line-height: 0.16rem;
cursor: pointer;
margin-bottom: 0.18rem;
flex-shrink: 0;
transition: opacity 0.2s;
text-align: center;
&:hover {
opacity: 0.85;
}
}
// 步骤提示
&__steps-tip {
font-size: 0.12rem;
color: $text-middle;
text-align: center;
margin: 0 0 0.14rem 0;
}
// 步骤卡片
&__step {
border: 1px solid $border-color;
border-radius: 0.08rem;
padding: 0.12rem 0.16rem;
margin-bottom: 0.1rem;
background: #F2F4F7;
}
&__step-title {
font-size: 0.13rem;
font-weight: 700;
color: $text-dark;
margin-bottom: 0.04rem;
}
&__step-desc {
font-size: 0.11rem;
color: $text-middle;
margin: 0;
line-height: 1.5;
}
// 统计数据
&__stats {
display: flex;
gap: 0.12rem;
margin-top: 0.16rem;
margin-bottom: 0.16rem;
align-items: center;
width: 3.64rem;
height: 0.46rem;
background: #F3FFFD;
border-radius: 0.14rem;
padding: 0 0.14rem;
margin: 0 auto 0.12rem;
box-sizing: border-box;
}
&__stat-item {
flex: 1;
border: 1px solid $border-color;
border-radius: 0.08rem;
padding: 0.12rem;
// 步骤数字圆圈
&__step-circle {
width: 0.26rem;
height: 0.26rem;
border-radius: 50%;
background: $bg-white;
border: 1px solid #BFEFE8;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.12rem;
font-weight: 700;
color: #12C7BE;
line-height: 0.15rem;
flex-shrink: 0;
box-sizing: border-box;
}
// 步骤文字
&__step-text {
margin-left: 0.14rem;
font-size: 0.13rem;
font-weight: 500;
color: #334155;
line-height: 0.16rem;
}
// 底部统计 + 按钮
&__footer {
display: flex;
align-items: center;
margin-top: auto;
padding-top: 0.06rem;
}
// 统计项
&__stat {
display: flex;
flex-direction: column;
margin-right: 0.2rem;
}
&__stat-label {
font-size: 0.11rem;
color: $text-middle;
margin-bottom: 0.06rem;
font-size: 0.13rem;
font-weight: 500;
color: #64748B;
line-height: 0.16rem;
}
&__stat-value {
font-size: 0.16rem;
font-weight: 700;
color: $text-dark;
font-size: 0.13rem;
font-weight: 500;
color: #64748B;
line-height: 0.16rem;
}
// 活动规则
&__rules {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid $border-color;
// 成为正式会员按钮
&__member-btn {
margin-left: auto;
width: 1.1rem;
height: 0.29rem;
background: #F3FFFD;
border: 1px solid #BFEFE8;
border-radius: 0.08rem;
padding: 0.12rem 0.16rem;
font-size: 0.13rem;
font-weight: 400;
color: #000000;
line-height: 0.16rem;
cursor: pointer;
transition: background 0.2s;
transition: all 0.2s;
text-align: center;
&:hover {
background: $bg-main;
}
}
&__rules-label {
font-size: 0.13rem;
font-weight: 600;
color: $text-dark;
}
&__rules-arrow {
width: 0.14rem;
height: 0.14rem;
color: $text-middle;
transition: transform 0.2s;
&--open {
transform: rotate(180deg);
}
}
&__rules-content {
margin-top: 0.1rem;
padding: 0.12rem 0.16rem;
background: $bg-main;
border-radius: 0.06rem;
p {
font-size: 0.11rem;
color: $text-middle;
line-height: 1.8;
margin: 0 0 0.04rem 0;
&:last-child {
margin-bottom: 0;
}
background: #e8faf8;
border-color: $accent;
}
}
}
+4 -5
View File
@@ -5,7 +5,7 @@
flex-direction: column;
width: 2rem;
height: var(--app-height, 100vh);
background: #1a1a2e;
background: $bg-white;
color: #fff;
padding: 0.2rem 0.12rem;
box-sizing: border-box;
@@ -57,7 +57,7 @@
align-items: center;
gap: 0.08rem;
padding: 0.1rem 0.12rem;
border-radius: 0.14rem;
border-radius: 0.08rem;
color: #9598ab;
text-decoration: none;
cursor: pointer;
@@ -65,12 +65,12 @@
font-size: 0.14rem;
&:hover {
background: rgba(255, 255, 255, 0.05);
background: $btn-dark;
color: #fff;
}
&--active {
background: #1F2937;
background: $btn-dark;
color: #fff;
}
}
@@ -98,7 +98,6 @@
display: flex;
flex-direction: column;
gap: 0.04rem;
border-top: 1px solid #2e3142;
padding-top: 0.12rem;
}
}
+6
View File
@@ -82,6 +82,12 @@
padding: 20px !important;
border-radius: 8px !important;
font-size: 14px !important;
// 修正 transform: scale 下弹窗居中
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
margin: 0 !important;
&__header {
padding: 0 0 12px 0 !important;
-97
View File
@@ -511,104 +511,7 @@
}
}
/* ==================== 简历上传阶段 — 左侧基础(step 3 使用) ==================== */
.login-view__left {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 57%;
height: 100%;
background: linear-gradient(180deg, #0A9E97 0%, #12C7BE 50%, #06B6D4 100%);
position: relative;
overflow: hidden;
}
/* ==================== 简历上传阶段 — 左侧职位卡片面板 ==================== */
.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 {
+35 -9
View File
@@ -354,6 +354,15 @@
gap: 0.06rem;
font-size: 0.12rem;
color: #666;
vertical-align: middle;
a{
color: #666;
vertical-align: super;
}
>span{
vertical-align: super;
}
}
&__contact-icon {
@@ -473,14 +482,7 @@
border-radius: 0.06rem;
}
// 经历条目内的编辑按钮hover exp-item 时显示)
&__exp-edit-btn {
}
&__exp-item:hover &__exp-edit-btn {
opacity: 1;
pointer-events: auto;
}
// 经历条目内的编辑按钮样式已在 &__exp-date 下方统一管理
// ---- 教育背景 ----
&__edu-item {
@@ -552,9 +554,33 @@
}
&__exp-date {
font-size: 0.12rem;
font-size: 0.13rem;
font-weight: 400;
color: $text-middle;
// 带切换效果的时间(hover exp-item 时隐藏)
&--toggle {
display: inline;
}
}
// hover经历条目时:隐藏时间,显示按钮
&__exp-item:hover &__exp-date--toggle {
display: none;
}
// 默认隐藏按钮且不占位
&__exp-edit-btn {
display: none;
padding-top: 0;
padding-bottom: 0;
}
// hover经历条目时:显示按钮
&__exp-item:hover &__exp-edit-btn {
display: flex;
opacity: 1;
pointer-events: auto;
}
// ---- 修复建议标记 ----
+130
View File
@@ -0,0 +1,130 @@
<template>
<!-- 全屏加载遮罩组件 白色卡片居中展示加载状态 -->
<Teleport to="body">
<Transition name="fullscreen-loading-fade">
<div v-if="visible" class="fullscreen-loading">
<!-- 半透明遮罩层 -->
<div class="fullscreen-loading__overlay"></div>
<!-- 白色内容卡片 -->
<div class="fullscreen-loading__card">
<!-- 顶部标题栏 logo仅在传入 title 时显示 -->
<div v-if="title" class="fullscreen-loading__header">
<img class="fullscreen-loading__logo dib" src="@/assets/images/logo-offerpai.png" alt="logo" />
<span class="fullscreen-loading__title dib ml8 verali-t pt2">{{ title }}</span>
</div>
<!-- 中间文案 -->
<p class="fullscreen-loading__text">{{ text }}</p>
<!-- 加载圆环动画 -->
<div class="fullscreen-loading__spinner"></div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
/** 全屏加载遮罩组件 */
defineProps<{
/** 是否显示 */
visible: boolean
/** 中间提示文案 */
text: string
/** 顶部标题文字(不传则不显示标题行和 logo) */
title?: string
}>()
</script>
<style lang="scss" scoped>
@use '../assets/styles/variables' as *;
.fullscreen-loading {
font-size: 0.14rem;
position: fixed;
inset: 0;
z-index: 2100;
display: flex;
align-items: center;
justify-content: center;
/* 半透明遮罩 */
&__overlay {
position: absolute;
inset: 0;
background: $overlay-bg;
}
/* 白色居中卡片 */
&__card {
position: relative;
background: $bg-white;
border-radius: 0.12rem;
padding: 0.7rem 0 0.5rem 0;
width: 4rem;
display: flex;
flex-direction: column;
align-items: center;
min-width: 3.6rem;
}
/* 顶部标题行 */
&__header {
position: absolute;
top: 0.12rem;
left: 0.2rem;
//display: flex;
//align-items: center;
//gap: 0.08rem;
margin-bottom: 0.3rem;
}
/* logo 图标 */
&__logo {
width: 0.28rem;
height: 0.28rem;
border-radius: 50%;
}
/* 标题文字 */
&__title {
font-size: 0.14rem;
color: $text-dark;
}
/* 中间文案 */
&__text {
font-size: 0.2rem;
font-weight: 600;
color: $text-dark;
margin-bottom: 0.28rem;
}
/* 旋转圆环 */
&__spinner {
width: 0.44rem;
height: 0.44rem;
border: 3px solid transparent;
border-top-color: $accent;
border-right-color: $accent;
border-bottom-color: $accent;
border-radius: 50%;
animation: fullscreen-loading-spin 1s linear infinite;
}
}
/* 旋转动画 */
@keyframes fullscreen-loading-spin {
to {
transform: rotate(360deg);
}
}
/* 淡入淡出过渡 */
.fullscreen-loading-fade-enter-active,
.fullscreen-loading-fade-leave-active {
transition: opacity 0.25s ease;
}
.fullscreen-loading-fade-enter-from,
.fullscreen-loading-fade-leave-to {
opacity: 0;
}
</style>
+11 -5
View File
@@ -347,6 +347,8 @@
</div>
</div>
</div>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
@@ -360,6 +362,7 @@ import type { ResumeListItem } from '@/api/resume'
import { fetchCustomizeResume, generateCustomizeResume, aiEditResume, rollbackCustomizeResume } from '@/api/jobs'
import type { CustomizeResumeData, AiEditChatMessage } from '@/api/jobs'
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
// ==================== 类型定义 ====================
@@ -415,6 +418,11 @@ const emit = defineEmits<{
// ==================== 步骤控制 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 当前步骤:1-确认入口(居中弹窗) 2-差距分析(右侧抽屉) 3-定制简历 4-预览 */
const currentStep = ref(1)
@@ -483,10 +491,8 @@ async function handleDrawerNext() {
* 流程:先 GET 查询 → 有数据直接用 → 无数据则 POST 生成 → 再 GET 查询
*/
async function fetchAndLoadCustomResume() {
const loadingInstance = ElLoading.service({
text: '正在生成定制简历...',
background: 'rgba(0, 0, 0, 0.5)',
})
fullLoadingText.value = '正在生成定制简历...'
fullLoading.value = true
try {
// 第一步:查询是否已有定制简历
let queryRes = await fetchCustomizeResume(props.jobId)
@@ -522,7 +528,7 @@ async function fetchAndLoadCustomResume() {
console.error('[JobResumeCustomDialog] 定制简历流程失败', e)
ElMessage.error('定制简历失败,请稍后重试')
} finally {
loadingInstance.close()
fullLoading.value = false
}
}
+13 -8
View File
@@ -232,14 +232,17 @@
</div>
</div>
</div>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useStore } from 'vuex'
import { ElMessage, ElLoading } from 'element-plus'
import { ElMessage } from 'element-plus'
import RegionSelector from '@/components/tools/RegionSelector.vue'
import { polishDiagnosisIssue } from '@/api/resume'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
/** 教育经历单条数据结构 */
interface EducationItem {
@@ -329,6 +332,12 @@ const emit = defineEmits<{
/** Vuex store 实例 */
const store = useStore()
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 模块名称映射表 */
const moduleTitleMap: Record<string, string> = {
info: '基本信息',
@@ -490,12 +499,8 @@ async function handleAiPolish() {
return
}
aiPolishing.value = true
const loading = ElLoading.service({
lock: true,
text: 'AI正在润色中……',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = 'AI正在润色中……'
fullLoading.value = true
try {
const res = await polishDiagnosisIssue([text])
if (res.code === 0 && res.data?.content) {
@@ -508,7 +513,7 @@ async function handleAiPolish() {
ElMessage.error('请求失败,请稍后重试')
} finally {
aiPolishing.value = false
loading.close()
fullLoading.value = false
}
}
+10 -9
View File
@@ -72,8 +72,9 @@
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ edu.school }}</div>
<div class="profile-page-content__item-sub">{{ getDegreeText(edu.studyType, edu.degree) }} · {{ edu.major }}</div>
<div class="profile-page-content__item-date">{{ edu.startDate }} - {{ edu.endDate }}</div>
</div>
<!-- 时间默认显示hover时隐藏 -->
<div class="profile-page-content__item-date profile-page-content__item-date--toggle">{{ edu.startDate }} - {{ edu.endDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -105,9 +106,9 @@
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ exp.companyName }}</div>
<div class="profile-page-content__item-sub">{{ exp.position }}</div>
<div class="profile-page-content__item-period">{{ exp.startDate }} - {{ exp.endDate || '至今' }}</div>
</div>
<!-- 时间默认显示hover时隐藏 -->
<div class="profile-page-content__item-period profile-page-content__item-date--toggle">{{ exp.startDate }} - {{ exp.endDate || '至今' }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -139,9 +140,9 @@
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ exp.companyName }}</div>
<div class="profile-page-content__item-sub">{{ exp.position }}</div>
<div class="profile-page-content__item-period">{{ exp.startDate }} - {{ exp.endDate }}</div>
</div>
<!-- 时间默认显示hover时隐藏 -->
<div class="profile-page-content__item-period profile-page-content__item-date--toggle">{{ exp.startDate }} - {{ exp.endDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -173,9 +174,9 @@
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ proj.projectName }}</div>
<div class="profile-page-content__item-sub" v-if="proj.role">{{ proj.role }}</div>
<div class="profile-page-content__item-period">{{ proj.startDate }} - {{ proj.endDate }}</div>
</div>
<!-- 时间默认显示hover时隐藏 -->
<div class="profile-page-content__item-period profile-page-content__item-date--toggle">{{ proj.startDate }} - {{ proj.endDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -206,9 +207,9 @@
<div class="profile-page-content__item-row">
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ comp.competitionName }} · {{ comp.award }}</div>
<div class="profile-page-content__item-period">{{ comp.awardDate }}</div>
</div>
<!-- 时间默认显示hover时隐藏 -->
<div class="profile-page-content__item-period profile-page-content__item-date--toggle">{{ comp.awardDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
+15 -12
View File
@@ -53,6 +53,8 @@
</div>
</div>
</Teleport>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
@@ -60,8 +62,8 @@ import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { uploadResume } from '@/utils/aiRequest'
import { syncProfileFromResume } from '@/api/profile'
import { ElLoading, ElMessage } from 'element-plus'
import 'element-plus/es/components/loading/style/css'
import { ElMessage } from 'element-plus'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
/** 组件 Props */
const props = defineProps<{ modelValue: boolean }>()
@@ -87,6 +89,11 @@ const uploading = ref(false)
/** 上传后获得的简历 ID */
const resumeId = ref('')
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 弹窗打开时重置状态 */
watch(() => props.modelValue, (val) => {
if (val) {
@@ -124,12 +131,8 @@ async function processFile(file: File) {
uploading.value = true
// 全屏加载
const loading = ElLoading.service({
lock: true,
text: '简历解析中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'profile-welcome-loading',
})
fullLoadingText.value = '简历解析中,请耐心等待…'
fullLoading.value = true
try {
// 上传简历文件
@@ -139,18 +142,18 @@ async function processFile(file: File) {
uploadedFileName.value = file.name
// 继续调用同步个人资料接口,加载特效延续
loading.setText('正在同步个人资料…')
fullLoadingText.value = '正在同步个人资料…'
await syncProfileFromResume(resumeId.value)
loading.close()
fullLoading.value = false
uploading.value = false
ElMessage.success('简历上传并同步成功')
} else {
loading.close()
fullLoading.value = false
uploading.value = false
ElMessage.error(res.msg || '上传失败')
}
} catch {
loading.close()
fullLoading.value = false
uploading.value = false
ElMessage.error('上传失败,请稍后重试')
}
+12 -3
View File
@@ -103,12 +103,15 @@
</div>
</div>
</div>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage, ElLoading } from 'element-plus'
import { ElMessage } from 'element-plus'
import { polishDiagnosisIssue } from '@/api/resume'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
/** 描述段落结构 */
interface DescriptionParagraph {
@@ -142,6 +145,11 @@ const aiPolishResult = ref('')
/** AI润色中 */
const aiPolishing = ref(false)
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 生成短ID */
const generateId = (): string => Math.random().toString(36).substring(2, 8)
@@ -177,7 +185,8 @@ async function handleAiPolish() {
return
}
aiPolishing.value = true
const loading = ElLoading.service({ lock: true, text: 'AI正在润色中……', background: 'rgba(0,0,0,0.5)' })
fullLoadingText.value = 'AI正在润色中……'
fullLoading.value = true
try {
const content = text.split('\n').filter(s => s.trim())
const res = await polishDiagnosisIssue(content)
@@ -191,7 +200,7 @@ async function handleAiPolish() {
ElMessage.error('请求失败')
} finally {
aiPolishing.value = false
loading.close()
fullLoading.value = false
}
}
+11 -7
View File
@@ -73,12 +73,15 @@
</div>
</Transition>
</Teleport>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import type { DiagnosisIssue, DescriptionParagraph } from '@/api/resume'
import { polishDiagnosisIssue, resolveDiagnosisIssue } from '@/api/resume'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
// ==================== Props ====================
@@ -169,6 +172,11 @@ const moduleOrder = ['summary', 'education', 'work', 'internship', 'project', 'c
// ==================== 响应式数据 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 所有模块分组后的数据列表 */
const moduleDataList = ref<ModuleGroup[]>([])
@@ -313,12 +321,8 @@ async function handleRegenerate(record: RecordState) {
return
}
const loading = ElLoading.service({
lock: true,
text: 'AI 重新生成中…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = 'AI 重新生成中…'
fullLoading.value = true
try {
const res = await polishDiagnosisIssue(content)
@@ -332,7 +336,7 @@ async function handleRegenerate(record: RecordState) {
} catch {
ElMessage.error('请求失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}
+15 -14
View File
@@ -138,6 +138,8 @@
</div>
</Transition>
</Teleport>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
@@ -145,6 +147,7 @@ import { computed, ref, watch, nextTick } from 'vue'
import type { DiagnosisIssue, DescriptionParagraph } from '@/api/resume'
import { polishDiagnosisIssue, feedbackDiagnosisIssue, resolveDiagnosisIssue } from '@/api/resume'
import { computeDiff, type DiffSegment } from '@/utils/textDiff'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
// ==================== Props ====================
@@ -182,6 +185,12 @@ const moduleTitleMap: Record<string, string> = {
/** 抽屉标题 */
const moduleTitle = computed(() => moduleTitleMap[props.moduleType] || '问题修复')
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
// ==================== 原文处理 ====================
/** 将原文内容统一转为字符串(description 数组的 text 逐行拼接) */
@@ -282,12 +291,8 @@ async function handleAiPolish() {
}
// 全屏加载提示
const loading = ElLoading.service({
lock: true,
text: 'AI 润色中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = 'AI 润色中,请耐心等待…'
fullLoading.value = true
try {
const res = await polishDiagnosisIssue(content)
@@ -300,7 +305,7 @@ async function handleAiPolish() {
} catch {
ElMessage.error('润色请求失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}
@@ -373,12 +378,8 @@ async function handleSubmit() {
}
// 全屏加载提示
const loading = ElLoading.service({
lock: true,
text: '正在提交新版本…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = '正在提交新版本…'
fullLoading.value = true
try {
// 1. 调用 resolve 接口标记问题已处理
@@ -394,7 +395,7 @@ async function handleSubmit() {
} catch {
ElMessage.error('提交失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}
+47 -46
View File
@@ -3,105 +3,106 @@
<Teleport to="body">
<div v-if="modelValue" class="settings-invite-overlay" @click="$emit('update:modelValue', false)">
<div class="settings-invite-dialog" @click.stop>
<!-- 顶部渐变区域 -->
<div class="settings-invite-dialog__header">
<!-- 皇冠图标 -->
<span class="settings-invite-dialog__crown"></span>
</div>
<!-- 关闭按钮 -->
<span class="settings-invite-dialog__close" @click="$emit('update:modelValue', false)"></span>
<!-- 内容区域 -->
<div class="settings-invite-dialog__body">
<!-- 标题 -->
<h2 class="settings-invite-dialog__title">邀请好友免费获取7天会员权益</h2>
<h2 class="settings-invite-dialog__title">邀请好友免费获取 7 天会员</h2>
<!-- 副标题 -->
<p class="settings-invite-dialog__subtitle">好友通过你的链接注册并完成认证 3 天会员</p>
<!-- 邀请链接区域 -->
<div class="settings-invite-dialog__link-box">
<p class="settings-invite-dialog__link-text">来一起领取AI求职助手会员{{ inviteText }}点击链接进入活动</p>
</div>
<!-- 复制链接按钮 -->
<span class="settings-invite-dialog__link-text">{{ inviteUrl }}</span>
<button class="settings-invite-dialog__copy-btn" @click="handleCopy">复制链接</button>
</div>
<!-- 步骤说明 -->
<p class="settings-invite-dialog__steps-tip">仅需 3 步可免费获取会员权益</p>
<div class="settings-invite-dialog__step">
<div class="settings-invite-dialog__step-title">第1步分享专属邀请链接</div>
<p class="settings-invite-dialog__step-desc">点击"复制链接"并发送给好友</p>
<div class="settings-invite-dialog__step-circle">1</div>
<span class="settings-invite-dialog__step-text">分享专属邀请链接给好友</span>
</div>
<div class="settings-invite-dialog__step">
<div class="settings-invite-dialog__step-title">第2步好友完成新用户首次体验</div>
<p class="settings-invite-dialog__step-desc">链接 24 小时内有效好友在时效内体验AI求职助手</p>
<div class="settings-invite-dialog__step-circle">2</div>
<span class="settings-invite-dialog__step-text">好友完成注册并通过认证</span>
</div>
<div class="settings-invite-dialog__step">
<div class="settings-invite-dialog__step-title">第3步分享人获得7天会员权益</div>
<p class="settings-invite-dialog__step-desc">免费会员权益一年内有效AI助力加速上岸</p>
<div class="settings-invite-dialog__step-circle">3</div>
<span class="settings-invite-dialog__step-text">获得 7 天会员奖励</span>
</div>
<!-- 统计数据 -->
<div class="settings-invite-dialog__stats">
<div class="settings-invite-dialog__stat-item">
<div class="settings-invite-dialog__stat-label">累计获得会员天数</div>
<div class="settings-invite-dialog__stat-value">{{ totalDays }}</div>
<!-- 底部统计 + 按钮 -->
<div class="settings-invite-dialog__footer">
<div class="settings-invite-dialog__stat">
<span class="settings-invite-dialog__stat-label">已邀请</span>
<span class="settings-invite-dialog__stat-value">{{ totalFriends }}</span>
</div>
<div class="settings-invite-dialog__stat-item">
<div class="settings-invite-dialog__stat-label">累计邀请好友</div>
<div class="settings-invite-dialog__stat-value">{{ totalFriends }}</div>
<div class="settings-invite-dialog__stat">
<span class="settings-invite-dialog__stat-label">累计获得</span>
<span class="settings-invite-dialog__stat-value">{{ totalDays }}</span>
</div>
<button class="settings-invite-dialog__member-btn" @click="handleGoMember">成为正式会员</button>
</div>
<!-- 活动规则折叠 -->
<div class="settings-invite-dialog__rules" @click="showRules = !showRules">
<span class="settings-invite-dialog__rules-label">活动规则</span>
<svg class="settings-invite-dialog__rules-arrow" :class="{ 'settings-invite-dialog__rules-arrow--open': showRules }" viewBox="0 0 12 12" fill="none">
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div v-if="showRules" class="settings-invite-dialog__rules-content">
<p>1. 每成功邀请一位新用户注册并完成首次体验邀请人可获得 7 天免费会员权益</p>
<p>2. 邀请链接有效期为 24 小时过期需重新生成</p>
<p>3. 累计获得的会员天数一年内有效超过一年未使用的天数将自动失效</p>
<p>4. 同一被邀请人仅计算一次重复邀请不累计奖励</p>
<p>5. 本活动最终解释权归平台所有</p>
</div>
</div>
</div>
</Teleport>
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import MemberDialog from './MemberDialog.vue'
/** 组件 Props */
defineProps<{ modelValue: boolean }>()
/** 组件 Emits */
defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const store = useStore()
/** 活动规则展开状态 */
const showRules = ref(false)
/** 会员弹窗显示状态 */
const showMemberDialog = ref(false)
/** 累计获得会员天数(暂用模拟数据) */
const totalDays = ref(1)
const totalDays = ref(7)
/** 累计邀请好友数(暂用模拟数据) */
const totalFriends = ref(1)
/** 用户邀请码 — 从全局 store 读取 */
const inviteCode = computed(() => store.state.userInfo?.inviteCode || '')
const inviteCode = computed(() => store.state.userInfo?.inviteCode || 'NOVA2026')
/** 邀请链接文案 */
const inviteText = computed(() => {
const code = inviteCode.value
return `https://www.offerpai.com.cn?invite_code=${code}`
})
/** 邀请链接地址 */
const inviteUrl = computed(() => `https://offerpai.com/invite?code=${inviteCode.value}`)
/** 复制链接到剪贴板 */
async function handleCopy() {
try {
await navigator.clipboard.writeText(inviteText.value)
await navigator.clipboard.writeText(inviteUrl.value)
ElMessage.success('链接已复制')
} catch {
ElMessage.error('复制失败,请手动复制')
}
}
/** 打开会员购买弹窗 */
function handleGoMember() {
showMemberDialog.value = true
}
</script>
+12 -5
View File
@@ -281,6 +281,8 @@
<MemberAccessDialog v-model="showMemberAccessDialog" @open-member="showMemberDialog = true" />
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</div>
</template>
@@ -296,6 +298,7 @@ import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
import MemberDialog from '@/components/MemberDialog.vue'
import { fetchJobDetail, toggleJobFavorite, removeJobFavorite, fetchSkillGap } from '@/api/jobs'
import { fetchMemberStatus } from '@/api/member'
@@ -307,6 +310,12 @@ const router = useRouter()
const route = useRoute()
const store = useStore()
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 当前岗位 ID,从路由参数中获取 */
const jobId = route.params.id as string
@@ -595,10 +604,8 @@ async function handleGenerateResume() {
// 接口异常时不阻断,继续执行
}
const loadingInstance = ElLoading.service({
text: '正在分析岗位匹配度...',
background: 'rgba(0, 0, 0, 0.5)',
})
fullLoadingText.value = '正在分析岗位匹配度...'
fullLoading.value = true
try {
const res = await fetchSkillGap(jobId)
if (res.code === 0 && res.data) {
@@ -609,7 +616,7 @@ async function handleGenerateResume() {
ElMessage.error('分析失败,请稍后重试')
return
} finally {
loadingInstance.close()
fullLoading.value = false
}
showResumeCustomDialog.value = true
}
+9 -47
View File
@@ -2,9 +2,7 @@
<!-- 登录页面 左右分栏布局包含登录和简历上传流程 -->
<div class="login-view">
<!-- ==================== 登录阶段step 1 & 2==================== -->
<template v-if="step === 1 || step === 2">
<!-- 左侧视觉动画面板 -->
<!-- ==================== 左侧视觉动画面板所有阶段共用 ==================== -->
<section class="login-visual" ref="loginVisualRef" aria-label="OfferPai AI job platform">
<div class="ai-hub-stage" ref="stageRef">
<!-- 标题 -->
@@ -23,8 +21,11 @@
<img class="login-jianshi-logo" :src="jianshiLogoSrc" alt="" aria-hidden="true" draggable="false" />
</section>
<!-- 右侧表单面板 -->
<!-- ==================== 右侧面板 ==================== -->
<div class="login-view__right">
<!-- ===== 登录阶段step 1 & 2===== -->
<template v-if="step === 1 || step === 2">
<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>
@@ -81,38 +82,10 @@
<span class="login-view__agreement-link" @click="openAgreement('hf8375i8')">隐私政策</span>
</div>
</div>
</div>
</template>
<!-- ==================== 简历上传阶段step 3==================== -->
<!-- ===== 简历上传阶段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上传区域 -->
@@ -151,9 +124,10 @@
</template>
</div>
</div>
</template>
</div>
<!-- 协议预览弹窗 -->
<AgreementPreviewDialog v-model="showAgreementDialog" :code="agreementCode" />
</div>
@@ -287,19 +261,7 @@ 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 = ['正在解析个人资料...', '正在解析教育背景...', '正在解析工作经历...', '正在解析技能特长...', '简历解析成功,即将跳转']
+16 -12
View File
@@ -37,6 +37,8 @@
</div>
</div>
</div>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
@@ -49,13 +51,19 @@ import ProfilePageContent from '@/components/ProfilePageContent.vue'
import ResumeUploadDialog from '@/components/ResumeUploadDialog.vue'
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition, syncProfileFromResume } from '@/api/profile'
import { uploadResume } from '@/utils/aiRequest'
import { ElMessage, ElLoading, ElMessageBox } from 'element-plus'
import 'element-plus/es/components/loading/style/css'
import { ElMessage, ElMessageBox } from 'element-plus'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
const router = useRouter()
const store = useStore()
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 页面挂载时加载公共分类数据和个人资料 */
onMounted(async () => {
if (!store.state.regions.length) {
@@ -208,29 +216,25 @@ const showWelcomeDialog = ref(false)
/** 欢迎弹窗确认上传回调 — 上传简历并同步个人资料,完成后刷新页面 */
async function handleWelcomeUpload(file: File) {
const loading = ElLoading.service({
lock: true,
text: '简历解析中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'profile-welcome-loading',
})
fullLoadingText.value = '简历解析中,请耐心等待…'
fullLoading.value = true
try {
const res = await uploadResume(file)
if (res.code === 0 && res.data?.resumeId) {
// 继续同步个人资料
loading.setText('正在同步个人资料…')
fullLoadingText.value = '正在同步个人资料…'
await syncProfileFromResume(String(res.data.resumeId))
loading.close()
fullLoading.value = false
ElMessage.success('简历上传并同步成功')
// 刷新页面数据
router.go(0)
} else {
loading.close()
fullLoading.value = false
ElMessage.error(res.msg || '上传失败')
}
} catch {
loading.close()
fullLoading.value = false
ElMessage.error('上传失败,请稍后重试')
}
}
+15 -11
View File
@@ -98,6 +98,8 @@
v-model="uploadDialogVisible"
@confirm="handleUploadConfirm"
/>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</div>
</template>
@@ -113,13 +115,19 @@ import { uploadResume } from '@/utils/aiRequest'
import {
fetchResumeList, deleteResume, setDefaultResume, type ResumeListItem,
} from '@/api/resume'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
// 命令式调用的组件,按需引入插件不会自动加载其样式,需手动引入
import 'element-plus/es/components/loading/style/css'
import 'element-plus/es/components/message-box/style/css'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
const router = useRouter()
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
// ==================== 头像颜色池 ====================
/** 头像背景色列表,按索引循环取色 */
@@ -297,12 +305,8 @@ function handleUpload() {
/** 上传弹窗确认回调 — 接收 File 对象,调用接口上传 */
async function handleUploadConfirm(file: File) {
// 全屏加载提示,AI 接口响应较慢
const loading = ElLoading.service({
lock: true,
text: '简历解析中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = '简历解析中,请耐心等待…'
fullLoading.value = true
try {
const res = await uploadResume(file)
@@ -311,14 +315,14 @@ async function handleUploadConfirm(file: File) {
loadResumeList()
// 等待让后端异步处理数据,再关闭加载动画并跳转详情页
await new Promise(resolve => setTimeout(resolve, 2000))
loading.close()
fullLoading.value = false
goDetail(res.data.resumeId)
} else {
loading.close()
fullLoading.value = false
ElMessage.error(res.msg || '上传失败')
}
} catch {
loading.close()
fullLoading.value = false
ElMessage.error('上传失败,请稍后重试')
}
}
+42 -42
View File
@@ -160,28 +160,30 @@
<rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M2 5.5l6 4 6-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ resumeMain.email }}
<span>{{ resumeMain.email }}</span>
</span>
<!-- 手机号 -->
<span v-if="resumeMain.mobileNumber" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M4 2h2.5l1.5 3-1.5 1.5a8 8 0 003 3L11 8l3 1.5V12a2 2 0 01-2 2C6.5 14 2 9.5 2 4a2 2 0 012-2z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
{{ resumeMain.mobileNumber }}
<span>{{ resumeMain.mobileNumber }}</span>
</span>
<!-- 微信号 -->
<span v-if="resumeMain.wechatNumber" class="resume-detail__contact-item">
<el-icon class="fs14 color-9"><ChatLineRound /></el-icon>
{{ resumeMain.wechatNumber }}
<span>{{ resumeMain.wechatNumber }}</span>
</span>
<!-- 作品集链接 -->
<span v-if="resumeMain.portfolioUrl" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M3 8a5 5 0 015-5h1a5 5 0 010 10H8a5 5 0 01-5-5z" stroke="currentColor" stroke-width="1.2"/>
<path d="M7 6l2 2-2 2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<span v-if="resumeMain.portfolioUrl" class="resume-detail__contact-item dib">
<svg xmlns="http://www.w3.org/2000/svg" class=" mr6" width="0.15rem" height="0.15rem" viewBox="0 0 16 16">
<path d="M0 0h16v16H0z" fill="none" />
<path fill="none" stroke="currentColor" d="M14.5 8A6.5 6.5 0 0 1 8 14.5M14.5 8A6.5 6.5 0 0 0 8 1.5M14.5 8c0 1.657-2.91 3-6.5 3S1.5 9.657 1.5 8m13 0c0-1.657-2.91-3-6.5-3S1.5 6.343 1.5 8M8 14.5A6.5 6.5 0 0 1 1.5 8M8 14.5c1.657 0 3-2.91 3-6.5S9.657 1.5 8 1.5m0 13c-1.657 0-3-2.91-3-6.5s1.343-6.5 3-6.5M1.5 8A6.5 6.5 0 0 1 8 1.5" />
</svg>
<a :href="resumeMain.portfolioUrl" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">{{ resumeMain.portfolioUrl }}</a>
<a :href="resumeMain.portfolioUrl?.startsWith('http') ? resumeMain.portfolioUrl : `http://${resumeMain.portfolioUrl}`" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">{{ resumeMain.portfolioUrl }}</a>
</span>
</div>
</template>
@@ -253,13 +255,14 @@
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ edu.school }}</span>
<span v-if="edu.major || edu.degree" class="resume-detail__exp-role">{{ edu.major }}{{ edu.degree ? '·' + edu.degree : '' }}</span>
<span v-if="edu.startDate || edu.endDate" class="resume-detail__exp-date">{{ edu.startDate }}<span v-if="edu.startDate && edu.endDate">-</span>{{ edu.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('education', edu.id!) && getIssueByRecord('education', edu.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('education', edu.id!)!.finding }}</span>
</span>
<div class="mlauto dflex-end">
<!-- 时间默认显示hover时隐藏 -->
<span v-if="edu.startDate || edu.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ edu.startDate }}<span v-if="edu.startDate && edu.endDate">-</span>{{ edu.endDate }}</span>
<!-- 编辑按钮 hover时显示 -->
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('education', edu)" aria-label="编辑">
<el-icon><EditPen /></el-icon>
@@ -302,13 +305,14 @@
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ exp.companyName }}</span>
<span v-if="exp.position" class="resume-detail__exp-role">{{ exp.position }}</span>
<span v-if="exp.startDate || exp.endDate" class="resume-detail__exp-date">{{ exp.startDate }}<span v-if="exp.startDate && exp.endDate">-</span>{{ exp.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('work', exp.id!) && getIssueByRecord('work', exp.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('work', exp.id!)!.finding }}</span>
</span>
<div class="mlauto dflex-end">
<!-- 时间默认显示hover时隐藏 -->
<span v-if="exp.startDate || exp.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ exp.startDate }}<span v-if="exp.startDate && exp.endDate">-</span>{{ exp.endDate }}</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('work', exp)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -339,13 +343,14 @@
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ intern.companyName }}</span>
<span v-if="intern.position" class="resume-detail__exp-role">{{ intern.position }}</span>
<span v-if="intern.startDate || intern.endDate" class="resume-detail__exp-date">{{ intern.startDate }}<span v-if="intern.startDate && intern.endDate">-</span>{{ intern.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('internship', intern.id!) && getIssueByRecord('internship', intern.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('internship', intern.id!)!.finding }}</span>
</span>
<div class="mlauto dflex-end">
<!-- 时间默认显示hover时隐藏 -->
<span v-if="intern.startDate || intern.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ intern.startDate }}<span v-if="intern.startDate && intern.endDate">-</span>{{ intern.endDate }}</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('internship', intern)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -376,13 +381,14 @@
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ proj.projectName }}</span>
<span v-if="proj.role" class="resume-detail__exp-role">{{ proj.role }}</span>
<span v-if="proj.startDate || proj.endDate" class="resume-detail__exp-date">{{ proj.startDate }}<span v-if="proj.startDate && proj.endDate">-</span>{{ proj.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('project', proj.id!) && getIssueByRecord('project', proj.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('project', proj.id!)!.finding }}</span>
</span>
<div class="mlauto dflex-end">
<!-- 时间默认显示hover时隐藏 -->
<span v-if="proj.startDate || proj.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ proj.startDate }}<span v-if="proj.startDate && proj.endDate">-</span>{{ proj.endDate }}</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('project', proj)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -413,13 +419,14 @@
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ comp.competitionName }}</span>
<span v-if="comp.award" class="resume-detail__exp-role">{{ comp.award }}</span>
<span v-if="comp.awardDate" class="resume-detail__exp-date">{{ comp.awardDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('competition', comp.id!) && getIssueByRecord('competition', comp.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('competition', comp.id!)!.finding }}</span>
</span>
<div class="mlauto dflex-end">
<!-- 时间默认显示hover时隐藏 -->
<span v-if="comp.awardDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ comp.awardDate }}</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('competition', comp)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
@@ -500,6 +507,8 @@
<MemberAccessDialog v-model="showMemberAccessDialog" @open-member="showMemberDialog = true" />
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</div>
</template>
@@ -557,11 +566,18 @@ import {
type DescriptionParagraph,
} from '@/api/resume'
import {ArrowLeft, EditPen, Plus, DeleteFilled, ChatLineRound} from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
// ==================== Store 实例 ====================
const store = useStore()
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
// ==================== 诊断数据相关 ====================
/** 会员权限拦截弹窗 */
@@ -936,12 +952,8 @@ async function handleDiagnose() {
}
// 全屏加载提示,AI 接口响应较慢
const loading = ElLoading.service({
lock: true,
text: '简历诊断中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = '简历诊断中,请耐心等待…'
fullLoading.value = true
try {
const res = await triggerResumeDiagnosis(resumeId)
if (res.code === 0) {
@@ -954,7 +966,7 @@ async function handleDiagnose() {
} catch {
ElMessage.error('诊断请求失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}
@@ -1098,12 +1110,8 @@ function joinDescriptionText(desc: DescriptionParagraph[]): string {
async function handleFixAllSave(items: FixAllSubmitItem[]) {
if (!items.length) return
const loading = ElLoading.service({
lock: true,
text: '正在保存修改…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = '正在保存修改…'
fullLoading.value = true
try {
for (const item of items) {
@@ -1123,7 +1131,7 @@ async function handleFixAllSave(items: FixAllSubmitItem[]) {
} catch {
ElMessage.error('保存失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}
@@ -1131,12 +1139,8 @@ async function handleFixAllSave(items: FixAllSubmitItem[]) {
async function handleFixAllUseAi(items: FixAllSubmitItem[]) {
if (!items.length) return
const loading = ElLoading.service({
lock: true,
text: '正在应用AI润色结果…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = '正在应用AI润色结果…'
fullLoading.value = true
try {
for (const item of items) {
@@ -1152,7 +1156,7 @@ async function handleFixAllUseAi(items: FixAllSubmitItem[]) {
} catch {
ElMessage.error('操作失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}
@@ -1215,12 +1219,8 @@ async function updateModuleContent(item: FixAllSubmitItem) {
/** 修复抽屉提交新版本 — 更新本地数据、调用保存接口、刷新页面 */
async function handleFixSubmit(content: string[]) {
const loading = ElLoading.service({
lock: true,
text: '正在保存修改…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
fullLoadingText.value = '正在保存修改…'
fullLoading.value = true
try {
const moduleType = fixModuleType.value
@@ -1262,7 +1262,7 @@ async function handleFixSubmit(content: string[]) {
} catch {
ElMessage.error('保存失败,请稍后重试')
} finally {
loading.close()
fullLoading.value = false
}
}