生成定制简历部分默认优化选项全选

This commit is contained in:
2026-07-06 18:58:16 +08:00
parent 6024ea27e9
commit 78bf7fbebc
8 changed files with 655 additions and 409 deletions
+14 -2
View File
@@ -435,6 +435,16 @@ export interface SkillGapResume {
targetPosition: string
}
/** 技能差距分析 — 差距项 */
export interface SkillGapItem {
/** 关键词 */
keyword: string
/** 差距标题 */
title: string
/** 差距描述 */
description: string
}
/** 技能差距分析返回数据 */
export interface SkillGapData {
/** 匹配度分数 */
@@ -443,8 +453,10 @@ export interface SkillGapData {
job: SkillGapJob
/** 简历信息 */
resume: SkillGapResume
/** 缺少的技能列表 */
missingSkills: string[]
/** 缺少的技能列表(兼容旧版) */
missingSkills?: string[]
/** 差距分析列表(新版) */
gaps?: SkillGapItem[]
}
/**
@@ -232,7 +232,7 @@
background: $btn-dark;
color: $bg-white;
border: none;
border-radius: 0.24rem;
border-radius: 0.08rem;
font-size: 0.15rem;
font-weight: 500;
cursor: pointer;
@@ -274,7 +274,6 @@
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-bottom: 0.02rem;
}
@@ -282,34 +281,44 @@
display: flex;
align-items: center;
gap: 0.08rem;
font-size: 0.13rem;
color: $text-light;
font-size: 0.14rem;
font-weight: 600;
color: $text-middle;
}
&__step--active {
color: $text-dark;
font-weight: 600;
}
&__step-num {
width: 0.22rem;
height: 0.22rem;
width: 0.32rem;
height: 0.32rem;
border-radius: 50%;
background: $bg-main;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.11rem;
font-weight: 600;
font-size: 0.14rem;
font-weight: 450;
color: $text-middle;
line-height: 1;
}
&__step--active &__step-num {
background: $text-dark;
background: $accent;
color: $bg-white;
font-weight: 600;
}
// ==================== 差距分析头部 ====================
// 步骤之间的连接横线
&__step-line {
width: 0.935rem;
height: 0.02rem;
background: #E5E7EB;
margin: 0 0.16rem;
}
// ==================== 差距分析头部(新版) ====================
&__gap-header {
display: flex;
align-items: flex-start;
@@ -323,28 +332,24 @@
}
&__gap-title {
font-size: 0.2rem;
font-weight: 700;
font-size: 0.24rem;
font-weight: 630;
color: $text-dark;
margin: 0 0 0.1rem 0;
line-height: 1.3;
margin: 0 0 0.08rem 0;
line-height: 0.32rem;
}
&__gap-warn {
display: flex;
align-items: flex-start;
gap: 0.06rem;
font-size: 0.12rem;
color: $text-middle;
line-height: 1.5;
// 匹配度高亮文字(品牌色)
&__gap-title-highlight {
color: #FE6D52;
}
&__warn-icon {
width: 0.14rem;
height: 0.14rem;
flex-shrink: 0;
margin-top: 0.02rem;
&__gap-subtitle {
font-size: 0.14rem;
font-weight: 330;
color: $text-middle;
margin: 0;
line-height: 0.19rem;
}
&__gap-score-area {
@@ -355,170 +360,184 @@
margin-left: 0.2rem;
}
// ==================== 对比卡片表格 ====================
&__gap-table {
margin-bottom: 0.36rem;
// 匹配度分数圆环
&__score-circle {
position: relative;
width: 0.72rem;
height: 0.72rem;
display: flex;
align-items: center;
justify-content: center;
}
&__score-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__score-num {
font-size: 0.24rem;
font-weight: 630;
color: $accent;
position: relative;
z-index: 1;
}
// 匹配度标签(底部黑底白字)
&__score-tag {
font-size: 0.14rem;
font-weight: 330;
color: $bg-white;
background: $text-dark;
border-radius: 0.04rem;
padding: 0.02rem 0.08rem;
margin-top: -0.1rem;
line-height: 0.19rem;
}
// ==================== 通用区域标题 ====================
&__section {
margin-bottom: 0.28rem;
}
&__section-title {
font-size: 0.18rem;
font-weight: 600;
color: $text-dark;
margin: 0 0 0.16rem 0;
line-height: 0.24rem;
}
// ==================== 简历选择下拉框 ====================
&__resume-select-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.12rem 0.16rem;
background: $bg-main;
border-radius: 0.12rem;
cursor: pointer;
position: relative;
width: 1.6rem;
}
&__resume-select-name {
font-size: 0.14rem;
font-weight: 600;
color: $text-middle;
line-height: 0.19rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__resume-select-arrow {
width: 0.14rem;
height: 0.14rem;
color: $text-middle;
flex-shrink: 0;
margin-left: 0.16rem;
}
// ==================== 岗位概览卡片 ====================
&__overview-card {
border: 1px solid #F0F0F0;
border-radius: 0.12rem;
padding: 0.16rem 0.2rem;
}
&__overview-title {
font-size: 0.14rem;
font-weight: 600;
color: $text-dark;
line-height: 0.19rem;
margin-bottom: 0.04rem;
}
&__overview-sub {
font-size: 0.14rem;
font-weight: 400;
color: $text-middle;
line-height: 0.19rem;
margin-bottom: 0.08rem;
}
&__overview-tags {
display: flex;
flex-wrap: wrap;
gap: 0.04rem;
}
&__overview-tag {
font-size: 0.12rem;
font-weight: 400;
color: $text-middle;
background: $bg-main;
border-radius: 0.04rem;
padding: 0.04rem 0.06rem;
line-height: 0.18rem;
}
// ==================== 差距分析列表 ====================
&__gap-list {
display: flex;
flex-direction: column;
gap: 0.06rem;
gap: 0.13rem;
}
&__gap-row {
display: flex;
gap: 0.06rem;
}
&__gap-cell {
flex: 1;
padding: 0.18rem 0.16rem;
font-size: 0.13rem;
color: $text-dark;
background: $bg-main;
border-radius: 0.08rem;
display: flex;
align-items: center;
min-height: 0.6rem;
}
&__gap-cell--label {
flex: 0 0 1.2rem;
font-weight: 600;
justify-content: flex-start;
}
&__gap-cell-title {
font-size: 0.18rem;
font-weight: 700;
}
// 关键词列横跨两列
&__gap-cell--keywords {
flex: 2.06;
}
// 概览行岗位信息
&__gap-job-info {
&__gap-item {
display: flex;
align-items: center;
padding: 0.16rem 0.2rem;
border: 1px solid #F0F0F0;
border-radius: 0.12rem;
gap: 0.12rem;
}
&__gap-company-icon {
width: 0.36rem;
height: 0.36rem;
border-radius: 0.06rem;
background: $bg-white;
// 序号圆圈
&__gap-item-num {
width: 0.32rem;
height: 0.32rem;
border-radius: 50%;
background: $accent;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
&__gap-company-logo {
width: 100%;
height: 100%;
border-radius: 0.06rem;
object-fit: cover;
}
&__gap-company-svg {
width: 0.2rem;
height: 0.2rem;
color: $text-dark;
}
&__gap-job-text {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
&__gap-job-title {
font-size: 0.14rem;
font-weight: 600;
color: $text-dark;
line-height: 1.2;
}
&__gap-job-sub {
font-size: 0.11rem;
color: $text-light;
line-height: 1.2;
}
// 岗位名称/简历目标值
&__gap-value {
font-size: 0.14rem;
font-weight: 500;
color: $text-dark;
}
// ==================== 简历选择器 ====================
&__resume-selector {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
position: relative;
}
&__resume-info {
display: flex;
align-items: center;
gap: 0.1rem;
}
&__resume-file-icon {
width: 0.2rem;
height: 0.2rem;
color: $text-dark;
flex-shrink: 0;
}
&__resume-text {
display: flex;
flex-direction: column;
gap: 0.02rem;
}
&__resume-label {
font-size: 0.1rem;
color: $text-light;
line-height: 1.2;
}
&__resume-name {
font-size: 0.12rem;
color: $text-dark;
font-weight: 500;
line-height: 1.2;
}
&__resume-select-btn {
display: flex;
align-items: center;
gap: 0.04rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.14rem;
padding: 0.05rem 0.12rem;
font-size: 0.11rem;
color: $text-dark;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:hover {
border-color: $accent;
color: $accent;
span {
font-size: 0.14rem;
font-weight: 600;
color: $bg-white;
line-height: 0.19rem;
}
}
&__dropdown-arrow {
width: 0.1rem;
height: 0.1rem;
&__gap-item-content {
flex: 1;
min-width: 0;
}
&__gap-item-title {
font-size: 0.14rem;
font-weight: 520;
color: $text-dark;
line-height: 0.19rem;
margin-bottom: 0.04rem;
}
&__gap-item-desc {
font-size: 0.14rem;
font-weight: 330;
color: $text-middle;
line-height: 0.19rem;
}
// ==================== 简历下拉选项(通用) ====================
&__resume-dropdown {
position: absolute;
top: 100%;
@@ -551,67 +570,80 @@
background: $theme-color;
}
// ==================== 岗位关键词标签 ====================
&__gap-keywords {
// ==================== 步骤三:定制简历(v2 新版上下布局) ====================
// 定制简历整体容器(纵向布局)
&__custom-v2 {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 0.32rem;
}
// 每个区块
&__custom-section {
display: flex;
flex-direction: column;
gap: 0.16rem;
}
// 区块头部(标题+描述)
&__custom-section-header {
display: flex;
flex-direction: column;
gap: 0.08rem;
}
&__gap-kw-tag {
font-size: 0.12rem;
background: $text-middle;
color: $bg-white;
border-radius: 0.04rem;
padding: 0.04rem 0.12rem;
line-height: 1.2;
// 区块标题
&__custom-section-title {
font-size: 0.18rem;
font-weight: 600;
color: #101828;
margin: 0;
line-height: 0.24rem;
}
// ==================== 步骤三:定制简历 ====================
// 定制简历左右两栏布局
&__custom {
display: flex;
gap: 0.16rem;
height: 100%;
// 区块描述
&__custom-section-desc {
font-size: 0.14rem;
font-weight: 400;
color: #99A1AF;
margin: 0;
line-height: 0.19rem;
}
// 左右面板
&__custom-panel {
flex: 1;
background: $bg-main;
border-radius: 0.12rem;
padding: 0.28rem 0.24rem;
overflow: hidden;
}
// 面板标题
&__custom-panel-title {
font-size: 0.17rem;
font-weight: 700;
color: $text-dark;
margin: 0 0 0.24rem 0;
line-height: 1.2;
}
// 选项列表
&__custom-options {
// 卡片选项列表
&__custom-cards {
display: flex;
flex-direction: column;
gap: 0.2rem;
gap: 0.13rem;
}
// 单个复选框行
&__custom-checkbox {
// 单个选项卡片
&__custom-card {
display: flex;
align-items: center;
padding: 0.16rem 0.2rem;
gap: 0.1rem;
border: 1px solid #F0F0F0;
border-radius: 0.12rem;
cursor: pointer;
position: relative;
transition: all 0.2s;
background: $bg-white;
&:hover {
border-color: transparent;
background: #F3F4F6;
}
}
// 选中态:无背景填充,仅品牌色边框
&__custom-card--checked {
border-color: #52CAD1;
background: $bg-white;
}
// 隐藏原生 checkbox
&__custom-input {
&__custom-card-input {
position: absolute;
opacity: 0;
width: 0;
@@ -619,63 +651,91 @@
pointer-events: none;
}
// 自定义复选框样式
&__custom-checkmark {
width: 0.18rem;
height: 0.18rem;
border: 1.5px solid $border-color;
border-radius: 0.04rem;
// 圆形复选框容器
&__custom-card-check {
width: 0.2rem;
height: 0.2rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
background: $bg-white;
}
// 选中态
&__custom-input:checked + &__custom-checkmark {
background: $accent;
border-color: $accent;
// 圆形复选框SVG
&__custom-card-check-svg {
width: 0.2rem;
height: 0.2rem;
}
&::after {
content: '';
display: block;
width: 0.05rem;
height: 0.09rem;
border: solid $bg-white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
margin-top: -0.04rem;
// 文字内容区域
&__custom-card-text {
display: flex;
align-items: center;
gap: 0.12rem;
flex: 1;
min-width: 0;
}
// 选项主文字(加粗)
&__custom-card-label {
font-size: 0.14rem;
font-weight: 600;
color: #1A1A1A;
line-height: 0.19rem;
white-space: nowrap;
}
// 选项说明文字(灰色小字)
&__custom-card-tip {
font-size: 0.12rem;
font-weight: 400;
color: #6A7282;
line-height: 0.16rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// ==================== 底部双按钮布局(上一步 + 下一步) ====================
&__footer-btns {
display: flex;
align-items: center;
gap: 0.24rem;
}
// 上一步按钮(品牌色边框 + 品牌色文字)
&__prev-btn {
flex: 1;
height: 0.45rem;
background: $bg-white;
border: 1px solid #52CAD1;
border-radius: 0.12rem;
font-size: 0.14rem;
font-weight: 600;
color: #52CAD1;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #F6FCFC;
}
}
// 选项文字
&__custom-label {
// 下一步按钮(品牌色填充 + 白色文字
&__next-btn {
flex: 1;
height: 0.45rem;
background: #52CAD1;
border: none;
border-radius: 0.12rem;
font-size: 0.14rem;
color: $text-dark;
line-height: 1.2;
user-select: none;
}
// 问号提示触发器
&__custom-tooltip-trigger {
display: flex;
align-items: center;
margin-left: 0.04rem;
//cursor: help;
font-weight: 600;
color: $bg-white;
cursor: pointer;
}
// 问号图标
&__custom-tooltip-icon {
width: 0.15rem;
height: 0.15rem;
color: $text-light;
transition: color 0.2s;
transition: background 0.2s;
&:hover {
color: $text-middle;
background: #47b8bf;
}
}
@@ -704,7 +764,6 @@
align-items: center;
gap: 0.12rem;
padding: 0.2rem 0.1rem;
border-bottom: 1px solid $border-color;
flex-shrink: 0;
}
@@ -731,23 +790,75 @@
}
// ==================== 步骤四:返回按钮 ====================
&__back-btn {
// 顶部下载简历按钮区域
&__top-download {
position: absolute;
top: 0.7rem;
left: 0.2rem;
z-index: 5;
}
&__top-download-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.04rem;
width: 1.04rem;
height: 0.36rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.2rem;
padding: 0.06rem 0.2rem;
border: 1px solid #52CAD1;
border-radius: 0.08rem;
font-size: 0.13rem;
color: $text-dark;
font-weight: 500;
color: #52CAD1;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #F6FCFC;
}
}
&__top-download-icon {
width: 0.14rem;
height: 0.14rem;
flex-shrink: 0;
}
// 顶部下载下拉菜单(向下弹出)
&__top-download-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 0.06rem;
width: 1.04rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.08rem;
box-shadow: 0 0.04rem 0.12rem rgba(0, 0, 0, 0.1);
overflow: hidden;
z-index: 10;
}
&__top-download-option {
display: block;
width: 100%;
padding: 0.1rem 0;
font-size: 0.12rem;
color: $text-dark;
background: none;
border: none;
cursor: pointer;
text-align: center;
transition: background 0.15s;
&:hover {
background: $bg-main;
}
& + & {
border-top: 1px solid $border-color;
}
}
// ==================== 步骤四:预览布局 ====================
@@ -1217,6 +1328,24 @@
}
}
// 步骤三底部:上一步按钮(品牌色边框样式)
&__footer-prev-btn {
flex: 1;
height: 0.48rem;
background: $bg-white;
border: 1px solid #52CAD1;
border-radius: 0.08rem;
font-size: 0.15rem;
font-weight: 500;
color: #52CAD1;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #F6FCFC;
}
}
// 立即去投递按钮
&__submit-btn {
flex: 1;
@@ -1224,7 +1353,7 @@
background: $btn-dark;
color: $bg-white;
border: none;
border-radius: 0.24rem;
border-radius: 0.08rem;
font-size: 0.15rem;
font-weight: 500;
cursor: pointer;
@@ -24,7 +24,7 @@
}
&__tab-item {
font-size: 0.14rem;
font-size: 0.18rem;
color: $text-middle;
cursor: pointer;
padding-bottom: 0.06rem;
@@ -86,7 +86,7 @@
}
&__card-title {
font-size: 0.16rem;
font-size: 0.20rem;
font-weight: 700;
color: $text-dark;
margin: 0;
+16 -3
View File
@@ -638,8 +638,12 @@
background: #FBFDF7;
}
&--medium {
background: #FFFBF5;
}
&--low {
background: #FBFDF7;
background: #FFF6F5;
}
}
@@ -686,13 +690,22 @@
color: #9FD051;
}
// 中匹配度颜色
&__job-match--medium &__match-score {
color: #F59E0B;
}
&__job-match--medium &__match-level {
color: #F59E0B;
}
// 低匹配度颜色
&__job-match--low &__match-score {
color: #9FD051;
color: #FE6D52;
}
&__job-match--low &__match-level {
color: #9FD051;
color: #FE6D52;
}
// 未登录锁定态
+49 -7
View File
@@ -22,18 +22,16 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { fetchFavoriteCount, fetchApplyCount } from '@/api/jobs'
// ==================== Props & Emits ====================
/** activeTab: 当前激活的 Tab key,由父组件通过 v-model 传入 */
const props = defineProps<{
activeTab: string
/** 收藏总数,由父组件传入 */
favoriteCount?: number
/** 投递总数,由父组件传入 */
applyCount?: number
}>()
/** 双向绑定事件,在 Jobs 页面内切换 Tab 时触发 */
@@ -45,14 +43,58 @@ const emit = defineEmits<{
const router = useRouter()
const route = useRoute()
const store = useStore()
/** 登录状态 */
const isAuthenticated = computed(() => store.state.isAuthenticated)
// ==================== 收藏 & 投递统计 ====================
/** 收藏总数 */
const favoriteTotal = ref(0)
/** 投递总数 */
const applyTotal = ref(0)
/** 加载收藏统计 */
async function loadFavoriteCount() {
try {
const res = await fetchFavoriteCount()
if (res.code === '0' && res.data) {
favoriteTotal.value = res.data.totalCount
}
} catch (e) {
console.error('加载收藏统计失败', e)
}
}
/** 加载投递统计 */
async function loadApplyCount() {
try {
const res = await fetchApplyCount()
if (res.code === '0' && res.data) {
applyTotal.value = res.data.totalCount
}
} catch (e) {
console.error('加载投递统计失败', e)
}
}
/** 组件挂载时请求收藏和投递统计 */
onMounted(() => {
if (isAuthenticated.value) {
loadFavoriteCount()
loadApplyCount()
}
})
// ==================== 常量数据 ====================
/** Tab 选项列表,count 用于显示圆形徽章计数 */
const tabs = computed(() => [
{ key: 'recommend', label: '推荐', count: undefined },
{ key: 'collected', label: '收藏', count: props.favoriteCount ?? 0 },
{ key: 'applied', label: '已投递', count: props.applyCount ?? 0 },
{ key: 'collected', label: '收藏', count: favoriteTotal.value },
{ key: 'applied', label: '已投递', count: applyTotal.value },
])
// ==================== 事件处理 ====================
+169 -124
View File
@@ -12,148 +12,168 @@
</button>
<h2 class="job-resume-custom-dialog__drawer-title">生成你的岗位专属简历</h2>
</div>
<!-- 返回按钮步骤显示 -->
<button v-if="currentStep === 4" class="job-resume-custom-dialog__back-btn mt10" @click="goToStep(3)">返回</button>
<!-- 下载简历按钮步骤显示位于步骤指示器上方 -->
<div v-if="currentStep === 4" class="job-resume-custom-dialog__top-download mt10">
<button class="job-resume-custom-dialog__top-download-btn" @click="toggleDownloadMenu">
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__top-download-icon">
<path d="M8 2v8M8 10l-3-3M8 10l3-3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 12h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
下载简历
</button>
<!-- 下载下拉菜单 -->
<div v-if="showDownloadMenu" class="job-resume-custom-dialog__top-download-menu">
<button class="job-resume-custom-dialog__top-download-option" @click="handleDownload('pdf')">下载PDF</button>
<button class="job-resume-custom-dialog__top-download-option" @click="handleDownload('word')">下载Word</button>
</div>
</div>
<!-- 步骤指示器 -->
<div class="job-resume-custom-dialog__steps pt10">
<div class="job-resume-custom-dialog__step" :class="{ 'job-resume-custom-dialog__step--active': currentStep === 2 }">
<span class="job-resume-custom-dialog__step-num">1</span><span>差距分析</span>
</div>
<div class="job-resume-custom-dialog__step-line"></div>
<div class="job-resume-custom-dialog__step" :class="{ 'job-resume-custom-dialog__step--active': currentStep === 3 }">
<span class="job-resume-custom-dialog__step-num">2</span><span>定制简历</span>
</div>
<div class="job-resume-custom-dialog__step-line"></div>
<div class="job-resume-custom-dialog__step" :class="{ 'job-resume-custom-dialog__step--active': currentStep === 4 }">
<span class="job-resume-custom-dialog__step-num">3</span><span>简历预览</span>
</div>
</div>
<!-- 抽屉内容区可滚动 -->
<div class="job-resume-custom-dialog__drawer-body">
<!-- 步骤差距分析 -->
<!-- 步骤差距分析 -->
<template v-if="currentStep === 2">
<!-- 匹配度标题区域 -->
<div class="job-resume-custom-dialog__gap-header">
<div class="job-resume-custom-dialog__gap-left">
<h3 class="job-resume-custom-dialog__gap-title">你的简历与该岗位的匹配度{{ isLowMatch ? '较低' : '较高' }}</h3>
<div v-if="isLowMatch" class="job-resume-custom-dialog__gap-warn">
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__warn-icon"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M8 5v3M8 10.5v.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
<span>匹配度低于 6.0 分的简历在筛选环节可能会被优先淘汰我们会帮你快速优化提升</span>
</div>
<h3 class="job-resume-custom-dialog__gap-title">你的简历与该岗位的<span class="job-resume-custom-dialog__gap-title-highlight">匹配度{{ isLowMatch ? '较低' : '较高' }}</span></h3>
<p class="job-resume-custom-dialog__gap-subtitle">匹配度低于 6.0 分的简历在筛选环节可能会被优先淘汰我们会帮你快速优化提升</p>
</div>
<!-- 右侧匹配度分数 -->
<div class="job-resume-custom-dialog__gap-score-area">
<div class="job-resume-custom-dialog__match-ring">
<svg viewBox="0 0 60 60" class="job-resume-custom-dialog__ring-svg"><circle cx="30" cy="30" r="24" stroke-width="4" stroke="#E8E8E8" fill="none" opacity="0.3"/><circle cx="30" cy="30" r="24" stroke-width="4" fill="none" stroke="#4FC2C9" stroke-linecap="round" :stroke-dasharray="2*Math.PI*24" :stroke-dashoffset="2*Math.PI*24*(1-jobInfo.matchScore/10)" transform="rotate(-90 30 30)"/></svg>
<span class="job-resume-custom-dialog__match-score">{{ jobInfo.matchScore }}</span>
<div class="job-resume-custom-dialog__score-circle">
<svg viewBox="0 0 72 72" class="job-resume-custom-dialog__score-svg">
<circle cx="36" cy="36" r="32" stroke-width="5" stroke="rgba(82,202,209,0.2)" fill="none"/>
<circle cx="36" cy="36" r="32" stroke-width="5" fill="none" stroke="#52CAD1" stroke-linecap="round" :stroke-dasharray="2*Math.PI*32" :stroke-dashoffset="2*Math.PI*32*(1-jobInfo.matchScore/10)" transform="rotate(-90 36 36)"/>
</svg>
<span class="job-resume-custom-dialog__score-num">{{ jobInfo.matchScore }}</span>
</div>
<span class="job-resume-custom-dialog__match-label">{{ matchLevelText }}</span>
<span class="job-resume-custom-dialog__score-tag">{{ matchLevelText }}</span>
</div>
</div>
<!-- 对比卡片表格 -->
<div class="job-resume-custom-dialog__gap-table">
<!-- 第一行概览 -->
<div class="job-resume-custom-dialog__gap-row">
<!-- 标签列 -->
<div class="job-resume-custom-dialog__gap-cell job-resume-custom-dialog__gap-cell--label">
<span class="job-resume-custom-dialog__gap-cell-title">概览</span>
</div>
<!-- 岗位信息列 -->
<div class="job-resume-custom-dialog__gap-cell">
<div class="job-resume-custom-dialog__gap-job-info">
<div class="job-resume-custom-dialog__gap-company-icon">
<img v-if="jobInfo.companyLogoUrl" :src="jobInfo.companyLogoUrl" :alt="jobInfo.company" class="job-resume-custom-dialog__gap-company-logo" />
<svg v-else viewBox="0 0 24 24" fill="none" class="job-resume-custom-dialog__gap-company-svg"><rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
</div>
<div class="job-resume-custom-dialog__gap-job-text">
<span class="job-resume-custom-dialog__gap-job-title">{{ jobInfo.title }}</span>
<span class="job-resume-custom-dialog__gap-job-sub">{{ jobInfo.location }} · {{ jobInfo.company }}&nbsp;&nbsp;独角兽</span>
</div>
</div>
</div>
<!-- 简历选择列 -->
<div class="job-resume-custom-dialog__gap-cell">
<div class="job-resume-custom-dialog__resume-selector">
<div class="job-resume-custom-dialog__resume-info">
<svg viewBox="0 0 24 24" fill="none" class="job-resume-custom-dialog__resume-file-icon"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14 2v6h6" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
<div class="job-resume-custom-dialog__resume-text">
<span class="job-resume-custom-dialog__resume-label">你的简历</span>
<span class="job-resume-custom-dialog__resume-name">{{ selectedResume.name }}</span>
</div>
</div>
<button class="job-resume-custom-dialog__resume-select-btn" @click="toggleResumeDropdown">选择 <svg viewBox="0 0 12 12" fill="none" class="job-resume-custom-dialog__dropdown-arrow"><path d="M3 5l3 3 3-3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<div v-if="showResumeDropdown" class="job-resume-custom-dialog__resume-dropdown">
<div v-for="r in resumeList" :key="r.id" class="job-resume-custom-dialog__resume-option" :class="{'job-resume-custom-dialog__resume-option--active': r.id === selectedResume.id}" @click="selectResume(r)">{{ r.name }}</div>
</div>
</div>
<!-- 简历选择 -->
<div class="job-resume-custom-dialog__section">
<h4 class="job-resume-custom-dialog__section-title">简历选择</h4>
<div class="job-resume-custom-dialog__resume-select-box" @click="toggleResumeDropdown">
<span class="job-resume-custom-dialog__resume-select-name">{{ selectedResume.name }}</span>
<svg viewBox="0 0 14 14" fill="none" class="job-resume-custom-dialog__resume-select-arrow"><path d="M4 6l3 3 3-3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<!-- 简历下拉 -->
<div v-if="showResumeDropdown" class="job-resume-custom-dialog__resume-dropdown">
<div v-for="r in resumeList" :key="r.id" class="job-resume-custom-dialog__resume-option" :class="{'job-resume-custom-dialog__resume-option--active': r.id === selectedResume.id}" @click.stop="selectResume(r)">{{ r.name }}</div>
</div>
</div>
<!-- 第二行岗位名称 -->
<div class="job-resume-custom-dialog__gap-row">
<div class="job-resume-custom-dialog__gap-cell job-resume-custom-dialog__gap-cell--label">
<span>岗位名称</span>
</div>
<!-- 岗位概览 -->
<div class="job-resume-custom-dialog__section">
<h4 class="job-resume-custom-dialog__section-title">岗位概览</h4>
<div class="job-resume-custom-dialog__overview-card">
<div class="job-resume-custom-dialog__overview-title">{{ jobInfo.company }} · {{ jobInfo.title }}</div>
<div class="job-resume-custom-dialog__overview-sub">{{ jobInfo.location }} · {{ jobInfo.company }}</div>
<div class="job-resume-custom-dialog__overview-tags">
<span v-for="kw in jobInfo.keywords" :key="kw" class="job-resume-custom-dialog__overview-tag">{{ kw }}</span>
</div>
<div class="job-resume-custom-dialog__gap-cell">
<span class="job-resume-custom-dialog__gap-value">{{ jobInfo.title }}</span>
</div>
<!-- <div class="job-resume-custom-dialog__gap-cell">
<span class="job-resume-custom-dialog__gap-value">{{ selectedResume.targetJob || '—' }}</span>
</div> -->
</div>
<!-- 第三行岗位关键词 -->
<div class="job-resume-custom-dialog__gap-row">
<div class="job-resume-custom-dialog__gap-cell job-resume-custom-dialog__gap-cell--label">
<span>岗位关键词</span>
</div>
<div class="job-resume-custom-dialog__gap-cell job-resume-custom-dialog__gap-cell--keywords">
<div class="job-resume-custom-dialog__gap-keywords">
<span v-for="kw in jobInfo.keywords" :key="kw" class="job-resume-custom-dialog__gap-kw-tag">{{ kw }}</span>
</div>
<!-- 差距分析列表 -->
<div class="job-resume-custom-dialog__section">
<h4 class="job-resume-custom-dialog__section-title">差距分析</h4>
<div class="job-resume-custom-dialog__gap-list">
<div v-for="(gap, idx) in gapItems" :key="idx" class="job-resume-custom-dialog__gap-item">
<div class="job-resume-custom-dialog__gap-item-num">
<span>{{ idx + 1 }}</span>
</div>
<div class="job-resume-custom-dialog__gap-item-content">
<div class="job-resume-custom-dialog__gap-item-title">{{ gap.title }}</div>
<div class="job-resume-custom-dialog__gap-item-desc">{{ gap.description }}</div>
</div>
</div>
</div>
</div>
</template>
<!-- 步骤三定制简历 -->
<!-- 步骤二定制简历 -->
<template v-if="currentStep === 3">
<div class="job-resume-custom-dialog__custom">
<!-- 左侧选择要优化的部分 -->
<div class="job-resume-custom-dialog__custom-panel">
<h3 class="job-resume-custom-dialog__custom-panel-title">1.选择你要优化的部分</h3>
<div class="job-resume-custom-dialog__custom-options">
<label v-for="item in optimizeSections" :key="item.key" class="job-resume-custom-dialog__custom-checkbox">
<input type="checkbox" v-model="item.checked" class="job-resume-custom-dialog__custom-input" />
<span class="job-resume-custom-dialog__custom-checkmark"></span>
<span class="job-resume-custom-dialog__custom-label">{{ item.label }}</span>
<!-- 技能和工作经验的问号提示使用 el-tooltip -->
<el-tooltip
v-if="item.tooltip"
:content="item.tooltip"
placement="right"
:show-arrow="true"
:popper-options="{ strategy: 'fixed' }"
effect="dark"
>
<span class="job-resume-custom-dialog__custom-tooltip-trigger">
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__custom-tooltip-icon">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/>
<path d="M6.5 6.5a1.5 1.5 0 112.12 1.37c-.42.18-.62.5-.62.88V9.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
<circle cx="8" cy="11.5" r="0.6" fill="currentColor"/>
</svg>
</span>
</el-tooltip>
<div class="job-resume-custom-dialog__custom-v2">
<!-- 第一部分选择要优化的部分 -->
<div class="job-resume-custom-dialog__custom-section">
<div class="job-resume-custom-dialog__custom-section-header">
<h3 class="job-resume-custom-dialog__custom-section-title">选择你要优化的部分</h3>
<p class="job-resume-custom-dialog__custom-section-desc">选择后系统会基于岗位要求自动重写对应模块提升简历通过初筛的概率</p>
</div>
<div class="job-resume-custom-dialog__custom-cards">
<label
v-for="item in optimizeSections"
:key="item.key"
class="job-resume-custom-dialog__custom-card"
:class="{ 'job-resume-custom-dialog__custom-card--checked': item.checked }"
>
<input type="checkbox" v-model="item.checked" class="job-resume-custom-dialog__custom-card-input" />
<!-- 自定义复选框圆形 -->
<span class="job-resume-custom-dialog__custom-card-check">
<svg v-if="item.checked" viewBox="0 0 20 20" fill="none" class="job-resume-custom-dialog__custom-card-check-svg">
<circle cx="10" cy="10" r="10" fill="#52CAD1"/>
<path d="M6 10.5l2.5 2.5 5.5-5.5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else viewBox="0 0 20 20" fill="none" class="job-resume-custom-dialog__custom-card-check-svg">
<circle cx="10" cy="10" r="9.33" stroke="#6A7282" stroke-width="1.33"/>
</svg>
</span>
<!-- 文字内容 -->
<span class="job-resume-custom-dialog__custom-card-text">
<span class="job-resume-custom-dialog__custom-card-label">{{ item.label }}</span>
<span v-if="item.tooltip" class="job-resume-custom-dialog__custom-card-tip">{{ item.tooltip }}</span>
</span>
</label>
</div>
</div>
<!-- 右侧选择要新增的技能关键词 -->
<div class="job-resume-custom-dialog__custom-panel">
<h3 class="job-resume-custom-dialog__custom-panel-title">2.选择你要新增的技能关键词</h3>
<div class="job-resume-custom-dialog__custom-options">
<label v-for="skill in newSkillOptions" :key="skill.name" class="job-resume-custom-dialog__custom-checkbox">
<input type="checkbox" v-model="skill.checked" class="job-resume-custom-dialog__custom-input" />
<span class="job-resume-custom-dialog__custom-checkmark"></span>
<span class="job-resume-custom-dialog__custom-label">{{ skill.name }}</span>
<!-- 第二部分选择要新增的技能关键词 -->
<div class="job-resume-custom-dialog__custom-section">
<div class="job-resume-custom-dialog__custom-section-header">
<h3 class="job-resume-custom-dialog__custom-section-title">选择你要新增的技能关键词</h3>
<p class="job-resume-custom-dialog__custom-section-desc">关键词会被自然融入经历描述中避免生硬堆砌</p>
</div>
<div class="job-resume-custom-dialog__custom-cards">
<label
v-for="skill in newSkillOptions"
:key="skill.name"
class="job-resume-custom-dialog__custom-card"
:class="{ 'job-resume-custom-dialog__custom-card--checked': skill.checked }"
>
<input type="checkbox" v-model="skill.checked" class="job-resume-custom-dialog__custom-card-input" />
<!-- 自定义复选框圆形 -->
<span class="job-resume-custom-dialog__custom-card-check">
<svg v-if="skill.checked" viewBox="0 0 20 20" fill="none" class="job-resume-custom-dialog__custom-card-check-svg">
<circle cx="10" cy="10" r="10" fill="#52CAD1"/>
<path d="M6 10.5l2.5 2.5 5.5-5.5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else viewBox="0 0 20 20" fill="none" class="job-resume-custom-dialog__custom-card-check-svg">
<circle cx="10" cy="10" r="9.33" stroke="#6A7282" stroke-width="1.33"/>
</svg>
</span>
<!-- 文字内容 -->
<span class="job-resume-custom-dialog__custom-card-text">
<span class="job-resume-custom-dialog__custom-card-label">{{ skill.name }}</span>
</span>
</label>
</div>
</div>
</div>
</template>
<!-- 步骤预览 -->
<!-- 步骤简历预览 -->
<template v-if="currentStep === 4">
<div class="job-resume-custom-dialog__preview">
<!-- 左侧简历模板预览支持差异对比模式 -->
@@ -280,18 +300,22 @@
</div>
<!-- 抽屉底部按钮 -->
<div v-if="currentStep < 4" class="job-resume-custom-dialog__drawer-footer">
<button class="job-resume-custom-dialog__primary-btn" @click="handleDrawerNext">立即定制简历</button>
</div>
<!-- 步骤四专属底部下载简历 + 立即去投递 -->
<div v-if="currentStep === 4" class="job-resume-custom-dialog__preview-footer mt10 pt30">
<!-- 左侧下载简历按钮带下拉 -->
<div class="job-resume-custom-dialog__download-wrap">
<button class="job-resume-custom-dialog__download-btn" @click="toggleDownloadMenu">下载简历</button>
<div v-if="showDownloadMenu" class="job-resume-custom-dialog__download-menu">
<button class="job-resume-custom-dialog__download-option" @click="handleDownload('pdf')">下载PDF</button>
<button class="job-resume-custom-dialog__download-option" @click="handleDownload('word')">下载Word</button>
<!-- 步骤二定制简历上一步 + 下一步 -->
<template v-if="currentStep === 3">
<div class="job-resume-custom-dialog__footer-btns">
<button class="job-resume-custom-dialog__prev-btn" @click="goToStep(2)">上一步</button>
<button class="job-resume-custom-dialog__next-btn" @click="handleDrawerNext">下一步</button>
</div>
</div>
</template>
<!-- 步骤一差距分析立即定制简历 -->
<template v-else>
<button class="job-resume-custom-dialog__primary-btn" @click="handleDrawerNext">立即定制简历</button>
</template>
</div>
<!-- 步骤三专属底部下载简历 + 立即去投递 -->
<div v-if="currentStep === 4" class="job-resume-custom-dialog__preview-footer mt10 pt30">
<!-- 左侧上一步按钮 -->
<button class="job-resume-custom-dialog__footer-prev-btn" @click="goToStep(3)">上一步</button>
<!-- 右侧立即去投递按钮 -->
<button class="job-resume-custom-dialog__submit-btn" @click="handleSubmit">立即去投递</button>
</div>
@@ -318,7 +342,7 @@ import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
import JobResumeCustomEditPanel from '@/components/JobResumeCustomEditPanel.vue'
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
import { exportResumePdf, exportResumeWord } from '@/utils/resumeExport'
import { fetchResumeList } from '@/api/resume'
import { fetchResumeList, setDefaultResume } from '@/api/resume'
import type { ResumeListItem } from '@/api/resume'
import { fetchCustomizeResume, generateCustomizeResume, aiEditResume, rollbackCustomizeResume } from '@/api/jobs'
import type { CustomizeResumeData, AiEditChatMessage } from '@/api/jobs'
@@ -334,6 +358,13 @@ interface ResumeOption {
targetJob: string
}
/** 差距分析项 */
interface GapItem {
keyword: string
title: string
description: string
}
/** 岗位信息 */
interface JobInfo {
title: string
@@ -344,6 +375,8 @@ interface JobInfo {
missingSkills: string[]
keywords: string[]
sourceUrl: string
/** 差距分析列表(来自 skill-gap 接口) */
gaps: GapItem[]
/** 默认简历信息(来自 skill-gap 接口) */
defaultResume: { resumeId: string; resumeName: string; targetPosition: string } | null
}
@@ -400,6 +433,9 @@ const matchLevelText = computed(() => {
/** 是否为低匹配度(低于6分) */
const isLowMatch = computed(() => props.jobInfo.matchScore < 6)
/** 差距分析列表(来自 jobInfo.gaps */
const gapItems = computed(() => props.jobInfo.gaps || [])
/** 跳转到指定步骤 */
function goToStep(step: number) {
// 从步骤4返回步骤3时,清理步骤4的状态数据(避免下次进入被旧数据影响)
@@ -440,6 +476,11 @@ async function handleDrawerNext() {
initSkillOptions()
currentStep.value++
} else if (currentStep.value === 3) {
// 校验:至少勾选一个优化部分
if (selectedSectionKeys.value.length === 0) {
ElMessage.warning('请选择你要优化的部分')
return
}
// 步骤3 → 步骤4:先调用定制简历接口
await fetchAndLoadCustomResume()
} else {
@@ -576,7 +617,7 @@ function handleClose() {
// ==================== 定制简历选项(步骤 ====================
// ==================== 定制简历选项(步骤 ====================
/** 优化部分选项 */
interface OptimizeSection {
@@ -594,19 +635,19 @@ interface SkillOption {
/** 左侧:可优化的简历部分 */
const optimizeSections = ref<OptimizeSection[]>([
{ key: 'summary', label: '个人概述', checked: false },
{ key: 'skills', label: '技能', checked: false, tooltip: '我们将把您勾选的技能补充进简历的技能模块。这对于简历能否通过ATS关键词筛选至关重要。' },
{ key: 'experience', label: '工作经验', checked: false, tooltip: '我们将把您选择的技能融入工作经历中,并对关键词进行润色,最大程度提升简历与岗位的匹配度。' },
{ key: 'summary', label: '个人概述', checked: true },
{ key: 'skills', label: '技能', checked: true, tooltip: '我们将把您勾选的技能补充进简历的技能模块。这对于简历能否通过ATS关键词筛选至关重要。' },
{ key: 'experience', label: '工作经验', checked: true, tooltip: '我们将把您选择的技能融入工作经历中,并对关键词进行润色,最大程度提升简历与岗位的匹配度。' },
])
/** 右侧:可新增的技能关键词 */
const newSkillOptions = ref<SkillOption[]>([])
/** 根据缺失技能初始化技能选项 */
/** 根据缺失技能初始化技能选项(默认全选) */
function initSkillOptions() {
newSkillOptions.value = missingSkills.value.map(skill => ({
name: skill,
checked: false,
checked: true,
}))
}
@@ -620,7 +661,7 @@ const selectedNewSkills = computed(() =>
newSkillOptions.value.filter(s => s.checked).map(s => s.name)
)
// ==================== 简历选择(步骤 ====================
// ==================== 简历选择(步骤 ====================
/** 简历列表(从接口获取) */
const resumeList = ref<ResumeOption[]>([])
@@ -680,13 +721,17 @@ function toggleResumeDropdown() {
showResumeDropdown.value = !showResumeDropdown.value
}
/** 选择简历 */
/** 选择简历并设为默认 */
function selectResume(r: ResumeOption) {
selectedResume.value = r
showResumeDropdown.value = false
// 调用设为默认简历接口
setDefaultResume(r.id).catch(e => {
console.error('[JobResumeCustomDialog] 设为默认简历失败', e)
})
}
// ==================== 步骤:预览相关 ====================
// ==================== 步骤:预览相关 ====================
/** 简历模板组件引用 */
const resumeTemplateRef = ref()
@@ -715,7 +760,7 @@ const customResumeRawData = ref<CustomizeResumeData>({
/** 当前预览右侧tabai-AI帮写 / edit-编辑 */
const previewTab = ref<'ai' | 'edit'>('ai')
/** AI优化结果列表(根据步骤勾选的优化项动态生成) */
/** AI优化结果列表(根据步骤勾选的优化项动态生成) */
const aiOptimizeResults = computed<string[]>(() => {
const results: string[] = []
+3 -1
View File
@@ -401,9 +401,11 @@ const resumeCustomJobInfo = computed(() => ({
companyLogoUrl: job.companyLogoUrl,
location: job.location,
matchScore: skillGapData.value?.score ?? +(job.matchScore / 10).toFixed(1),
missingSkills: skillGapData.value?.missingSkills || [],
missingSkills: skillGapData.value?.missingSkills || skillGapData.value?.gaps?.map(g => g.keyword) || [],
keywords: skillGapData.value?.job?.skillTags || job.requiredSkills.map(s => s.name),
sourceUrl: job.sourceUrl,
/** 差距分析列表(来自 skill-gap 接口) */
gaps: skillGapData.value?.gaps || [],
/** 默认简历信息(来自 skill-gap 接口) */
defaultResume: skillGapData.value?.resume || null,
}))
+9 -6
View File
@@ -15,7 +15,7 @@
</div>
<!-- 页面标题 + Tab 切换 -->
<JobPageHeader v-model:activeTab="activeTab" :favoriteCount="favoriteTotal" :applyCount="applyTotal" />
<JobPageHeader v-model:activeTab="activeTab" />
<!-- 筛选条件 -->
<div v-if="isRecommendTab" class="jobs-page__filters-bar">
@@ -214,7 +214,7 @@
<template v-if="isAuthenticated">
<div class="jobs-page__match-ring">
<svg viewBox="0 0 80 80" class="jobs-page__ring-svg">
<circle cx="40" cy="40" r="34" stroke-width="5" stroke="#9FD051" fill="none" opacity="0.3"/>
<circle cx="40" cy="40" r="34" stroke-width="5" :stroke="matchRingColor(job.matchScore)" fill="none" opacity="0.3"/>
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
:stroke="matchRingColor(job.matchScore)"
stroke-linecap="round"
@@ -1194,20 +1194,23 @@ async function handleRemoveFromList(job: JobItem) {
/** 根据匹配分数返回对应的 CSS 类名 */
function matchClass(score: number) {
return score >= 80 ? 'jobs-page__job-match--high' : 'jobs-page__job-match--low'
if (score >= 80) return 'jobs-page__job-match--high'
if (score >= 40) return 'jobs-page__job-match--medium'
return 'jobs-page__job-match--low'
}
/** 根据匹配分数返回环形颜色 */
function matchRingColor(score: number) {
if (score >= 80) return '#9FD051'
return '#9FD051'
if (score >= 40) return '#F59E0B'
return '#FE6D52'
}
/** 根据匹配分数返回匹配等级文案 */
function matchLevelText(score: number) {
if (score >= 80) return '高匹配度'
if (score >= 60) return '一般匹配'
return '匹配度偏低'
if (score >= 40) return '匹配'
return '匹配度'
}
/** 跳转到岗位详情页 — 未登录弹登录框,登录后再跳转 */