1033 lines
44 KiB
Vue
1033 lines
44 KiB
Vue
<template>
|
||
<!-- 岗位专属简历定制弹窗:步骤1居中弹窗,步骤2+右侧抽屉 -->
|
||
<div v-if="modelValue" class="job-resume-custom-dialog" :class="{ 'job-resume-custom-dialog--drawer': currentStep >= 2 }">
|
||
<div class="job-resume-custom-dialog__overlay" @click="handleClose"></div>
|
||
<!-- ===== 步骤一:居中弹窗 ===== -->
|
||
<div v-if="currentStep === 1" class="job-resume-custom-dialog__panel">
|
||
<div class="job-resume-custom-dialog__header">
|
||
<h2 class="job-resume-custom-dialog__title">10s快速定制岗位专属简历</h2>
|
||
<button class="job-resume-custom-dialog__close-btn" @click="handleClose" aria-label="关闭">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__close-icon"><path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
</button>
|
||
</div>
|
||
<div v-if="isLowMatch" class="job-resume-custom-dialog__tip-bar"><span>你与该岗位的匹配度较低,简历可能无法通过机筛。</span></div>
|
||
<div class="job-resume-custom-dialog__job-card">
|
||
<div class="job-resume-custom-dialog__job-left">
|
||
<div class="job-resume-custom-dialog__company-icon">
|
||
<img v-if="jobInfo.companyLogoUrl" :src="jobInfo.companyLogoUrl" :alt="jobInfo.company" class="job-resume-custom-dialog__company-logo-img" />
|
||
<svg v-else viewBox="0 0 24 24" fill="none" class="job-resume-custom-dialog__company-svg"><rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/><path d="M3 13h18" stroke="currentColor" stroke-width="1.5"/></svg>
|
||
</div>
|
||
<div class="job-resume-custom-dialog__job-info">
|
||
<span class="job-resume-custom-dialog__job-title">{{ jobInfo.title }}</span>
|
||
<span class="job-resume-custom-dialog__job-sub">{{ jobInfo.location }} · {{ jobInfo.company }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="job-resume-custom-dialog__match-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>
|
||
<span class="job-resume-custom-dialog__match-label">{{ matchLevelText }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="job-resume-custom-dialog__skills-section">
|
||
<p class="job-resume-custom-dialog__skills-title">缺少{{ missingSkills.length }}项技能</p>
|
||
<div class="job-resume-custom-dialog__skills-list">
|
||
<span v-for="skill in missingSkills" :key="skill" class="job-resume-custom-dialog__skill-tag">{{ skill }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="job-resume-custom-dialog__footer">
|
||
<button class="job-resume-custom-dialog__primary-btn" @click="goToStep(2)">立即定制简历</button>
|
||
<span class="job-resume-custom-dialog__skip-link" @click="handleSkip">不优化,直接投递</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 步骤2+:右侧抽屉 ===== -->
|
||
<div v-if="currentStep >= 2" class="job-resume-custom-dialog__drawer">
|
||
<!-- 抽屉头部 -->
|
||
<div class="job-resume-custom-dialog__drawer-header">
|
||
<button class="job-resume-custom-dialog__close-btn" @click="handleClose" aria-label="关闭">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__close-icon"><path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
</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 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" :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" :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>
|
||
</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>
|
||
<span class="job-resume-custom-dialog__match-label">{{ 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 }} 独角兽</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>
|
||
</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">
|
||
<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>
|
||
</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>
|
||
</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>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<!-- 步骤四:预览 -->
|
||
<template v-if="currentStep === 4">
|
||
<div class="job-resume-custom-dialog__preview">
|
||
<!-- 左侧:简历模板预览(支持差异对比模式) -->
|
||
<div class="job-resume-custom-dialog__preview-left">
|
||
<JobResumeTemplate
|
||
:resumeData="resumeTemplateData"
|
||
:showDiff="isShowDiff"
|
||
:oldResumeData="oldResumeTemplateData"
|
||
ref="resumeTemplateRef"
|
||
/>
|
||
</div>
|
||
<!-- 右侧:AI帮写 / 编辑 tab -->
|
||
<div class="job-resume-custom-dialog__preview-right">
|
||
<!-- Tab 切换 -->
|
||
<div class="job-resume-custom-dialog__preview-tabs">
|
||
<button
|
||
class="job-resume-custom-dialog__preview-tab"
|
||
:class="{ 'job-resume-custom-dialog__preview-tab--active': previewTab === 'ai' }"
|
||
@click="previewTab = 'ai'"
|
||
>AI帮写</button>
|
||
<button
|
||
class="job-resume-custom-dialog__preview-tab"
|
||
:class="{ 'job-resume-custom-dialog__preview-tab--active': previewTab === 'edit' }"
|
||
@click="previewTab = 'edit'"
|
||
>编辑</button>
|
||
</div>
|
||
<!-- AI帮写内容 -->
|
||
<div v-if="previewTab === 'ai'" class="job-resume-custom-dialog__preview-ai">
|
||
|
||
<!-- AI聊天消息区域 -->
|
||
<div class="job-resume-custom-dialog__ai-messages" ref="aiMessagesRef">
|
||
|
||
<!-- 匹配度提升提示 -->
|
||
<div class="job-resume-custom-dialog__ai-result prl0">
|
||
<div class="job-resume-custom-dialog__ai-result-text">
|
||
<p class="job-resume-custom-dialog__ai-result-title">恭喜!你的简历匹配值从<br/>{{ jobInfo.matchScore }}分提升到了{{ cachedOptimizedScore }}分!</p>
|
||
<div class="job-resume-custom-dialog__ai-result-detail">
|
||
<p class="job-resume-custom-dialog__ai-result-subtitle">做了哪些优化?</p>
|
||
<ul class="job-resume-custom-dialog__ai-result-list">
|
||
<li v-for="(item, i) in aiOptimizeResults" :key="i">·{{ item }}</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="job-resume-custom-dialog__ai-result-score">
|
||
<div class="job-resume-custom-dialog__match-ring job-resume-custom-dialog__match-ring--large">
|
||
<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-cachedOptimizedScore/10)" transform="rotate(-90 30 30)"/>
|
||
</svg>
|
||
<span class="job-resume-custom-dialog__match-score">{{ cachedOptimizedScore.toFixed(1) }}</span>
|
||
</div>
|
||
<span class="job-resume-custom-dialog__match-label job-resume-custom-dialog__match-label--high">{{ cachedOptimizedScore >= 9 ? '非常匹配' : cachedOptimizedScore >= 6 ? '高匹配度' : '低匹配度' }}</span>
|
||
</div>
|
||
</div>
|
||
<!-- 快捷操作按钮 -->
|
||
<div class="job-resume-custom-dialog__ai-quick-actions prl0">
|
||
<button
|
||
v-for="(action, i) in aiQuickActions"
|
||
:key="i"
|
||
class="job-resume-custom-dialog__ai-quick-btn"
|
||
@click="sendAiMessage(action)"
|
||
>{{ action }}</button>
|
||
</div>
|
||
|
||
<div
|
||
v-for="(msg, i) in aiMessages"
|
||
:key="i"
|
||
class="job-resume-custom-dialog__ai-msg-wrap"
|
||
>
|
||
<div
|
||
class="job-resume-custom-dialog__ai-msg"
|
||
:class="msg.role === 'assistant' ? 'job-resume-custom-dialog__ai-msg--ai' : 'job-resume-custom-dialog__ai-msg--user'"
|
||
>
|
||
<div class="job-resume-custom-dialog__ai-msg-bubble">{{ msg.content }}</div>
|
||
</div>
|
||
<!-- 撤销修改气泡 -->
|
||
<!-- 已撤销状态:所有历史中已撤销的消息都显示 -->
|
||
<div v-if="msg.canRollback && msg.rollbackStatus === 'done'" class="job-resume-custom-dialog__ai-rollback">
|
||
<span class="job-resume-custom-dialog__ai-rollback-done">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__ai-rollback-icon">
|
||
<path d="M3 8.5l3 3 7-7" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
已撤销此次简历修改
|
||
</span>
|
||
</div>
|
||
<!-- 撤销按钮:仅当该消息是列表最后一条且为AI修改简历的回答且未撤销时显示 -->
|
||
<div v-if="msg.canRollback && msg.rollbackStatus !== 'done' && isLastMessage(i)" class="job-resume-custom-dialog__ai-rollback">
|
||
<button
|
||
class="job-resume-custom-dialog__ai-rollback-btn"
|
||
@click="handleRollbackClick(i)"
|
||
>
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__ai-rollback-icon">
|
||
<path d="M3 8h7a3 3 0 010 6H8M3 8l3-3M3 8l3 3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
撤销修改
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- AI正在回复的加载指示器 -->
|
||
<AiThinkingIndicator v-if="aiLoading" text="AI正在思考中" />
|
||
</div>
|
||
<!-- AI输入框 -->
|
||
<div class="job-resume-custom-dialog__ai-input-area">
|
||
<input
|
||
v-model="aiInputText"
|
||
class="job-resume-custom-dialog__ai-input"
|
||
placeholder="你要怎么优化"
|
||
@keyup.enter="sendAiMessage(aiInputText)"
|
||
/>
|
||
<button class="job-resume-custom-dialog__ai-send-btn" @click="sendAiMessage(aiInputText)">
|
||
<svg viewBox="0 0 24 24" fill="none" class="job-resume-custom-dialog__ai-send-icon">
|
||
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- 编辑内容 — 折叠手风琴式编辑面板 -->
|
||
<div v-if="previewTab === 'edit'" class="job-resume-custom-dialog__preview-edit">
|
||
<JobResumeCustomEditPanel :resumeData="customResumeRawData" :jobId="jobId" @update="onEditPanelUpdate" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</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>
|
||
</div>
|
||
</div>
|
||
<!-- 右侧:立即去投递按钮 -->
|
||
<button class="job-resume-custom-dialog__submit-btn" @click="handleSubmit">立即去投递</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 撤销修改确认弹窗 -->
|
||
<div v-if="showRollbackConfirm" class="job-resume-custom-dialog__rollback-confirm-overlay">
|
||
<div class="job-resume-custom-dialog__rollback-confirm">
|
||
<p class="job-resume-custom-dialog__rollback-confirm-text">确定要撤销AI助手对简历的修改吗?</p>
|
||
<div class="job-resume-custom-dialog__rollback-confirm-actions">
|
||
<button class="job-resume-custom-dialog__rollback-confirm-cancel" @click="cancelRollback">取消</button>
|
||
<button class="job-resume-custom-dialog__rollback-confirm-ok" @click="confirmRollback">确定撤销</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, nextTick, watch } from 'vue'
|
||
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 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'
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
/** 简历选项 */
|
||
interface ResumeOption {
|
||
id: string
|
||
name: string
|
||
targetJob: string
|
||
}
|
||
|
||
/** 岗位信息 */
|
||
interface JobInfo {
|
||
title: string
|
||
company: string
|
||
companyLogoUrl: string
|
||
location: string
|
||
matchScore: number
|
||
missingSkills: string[]
|
||
keywords: string[]
|
||
sourceUrl: string
|
||
/** 默认简历信息(来自 skill-gap 接口) */
|
||
defaultResume: { resumeId: string; resumeName: string; targetPosition: string } | null
|
||
}
|
||
|
||
/** AI聊天消息(用于界面展示) */
|
||
interface AiChatMsg {
|
||
/** 角色:user-用户 assistant-AI助手 */
|
||
role: 'user' | 'assistant'
|
||
/** 消息内容 */
|
||
content: string
|
||
/** 是否可以撤销(仅 type=updated 的 assistant 消息有此标记) */
|
||
canRollback?: boolean
|
||
/** 撤销状态:idle-未操作 done-已撤销 */
|
||
rollbackStatus?: 'idle' | 'done'
|
||
}
|
||
|
||
// ==================== Props & Emits ====================
|
||
|
||
const props = defineProps<{
|
||
/** 控制弹窗显隐 */
|
||
modelValue: boolean
|
||
/** 岗位信息 */
|
||
jobInfo: JobInfo
|
||
/** 岗位 ID(字符串,避免大整数精度丢失) */
|
||
jobId: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', val: boolean): void
|
||
(e: 'skip'): void
|
||
(e: 'submit'): void
|
||
}>()
|
||
|
||
// ==================== 步骤控制 ====================
|
||
|
||
/** 当前步骤:1-确认入口(居中弹窗) 2-差距分析(右侧抽屉) 3-定制简历 4-预览 */
|
||
const currentStep = ref(1)
|
||
|
||
/** 缺少的技能列表 */
|
||
const missingSkills = computed(() => props.jobInfo.missingSkills || [])
|
||
|
||
/** 匹配度等级文案(6分为高低分界线) */
|
||
const matchLevelText = computed(() => {
|
||
const score = props.jobInfo.matchScore
|
||
if (score >= 6) return '高匹配度'
|
||
return '低匹配度'
|
||
})
|
||
|
||
/** 是否为低匹配度(低于6分) */
|
||
const isLowMatch = computed(() => props.jobInfo.matchScore < 6)
|
||
|
||
/** 跳转到指定步骤 */
|
||
function goToStep(step: number) {
|
||
// 从步骤4返回步骤3时,清理步骤4的状态数据(避免下次进入被旧数据影响)
|
||
if (currentStep.value === 4 && step === 3) {
|
||
resetStep4State()
|
||
}
|
||
if (step === 3) initSkillOptions()
|
||
if (step === 4) fetchAndLoadCustomResume()
|
||
else currentStep.value = step
|
||
}
|
||
|
||
/** 重置步骤4(预览)的所有状态数据 */
|
||
function resetStep4State() {
|
||
aiMessages.value = []
|
||
aiInputText.value = ''
|
||
aiLoading.value = false
|
||
isShowDiff.value = false
|
||
previewTab.value = 'ai'
|
||
cachedOptimizedScore.value = 0
|
||
oldResumeTemplateData.value = {
|
||
name: '', email: '', mobileNumber: '', wechatNumber: '', summary: '',
|
||
educations: [], workExperiences: [], internships: [], projects: [],
|
||
competitions: [], skills: [], certificates: [],
|
||
}
|
||
resumeTemplateData.value = {
|
||
name: '', email: '', mobileNumber: '', wechatNumber: '', summary: '',
|
||
educations: [], workExperiences: [], internships: [], projects: [],
|
||
competitions: [], skills: [], certificates: [],
|
||
}
|
||
customResumeRawData.value = { resume: {} }
|
||
showDownloadMenu.value = false
|
||
}
|
||
|
||
/** 抽屉模式下一步 */
|
||
async function handleDrawerNext() {
|
||
if (currentStep.value >= 4) return
|
||
if (currentStep.value === 2) {
|
||
initSkillOptions()
|
||
currentStep.value++
|
||
} else if (currentStep.value === 3) {
|
||
// 步骤3 → 步骤4:先调用定制简历接口
|
||
await fetchAndLoadCustomResume()
|
||
} else {
|
||
currentStep.value++
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取定制简历数据并跳转到预览步骤
|
||
* 流程:先 GET 查询 → 有数据直接用 → 无数据则 POST 生成 → 再 GET 查询
|
||
*/
|
||
async function fetchAndLoadCustomResume() {
|
||
const loadingInstance = ElLoading.service({
|
||
text: '正在生成定制简历...',
|
||
background: 'rgba(0, 0, 0, 0.5)',
|
||
})
|
||
try {
|
||
// 第一步:查询是否已有定制简历
|
||
let queryRes = await fetchCustomizeResume(props.jobId)
|
||
// if (queryRes.code === 0 && queryRes.data) {
|
||
// // 已有定制简历,直接填充数据并跳转预览
|
||
// fillCustomResumeData(queryRes.data)
|
||
// currentStep.value = 4
|
||
// return
|
||
// }
|
||
|
||
// 第二步:没有定制简历,调用生成接口
|
||
const genRes = await generateCustomizeResume({
|
||
jobId: props.jobId,
|
||
resumeId: selectedResume.value.id,
|
||
optimizeModules: selectedSectionKeys.value,
|
||
addSkills: selectedNewSkills.value.length > 0 ? selectedNewSkills.value : undefined,
|
||
})
|
||
|
||
if (genRes.code !== 0 || !genRes.data?.success) {
|
||
ElMessage.error('生成定制简历失败,请稍后重试')
|
||
return
|
||
}
|
||
|
||
// 第三步:生成成功后再次查询获取简历数据
|
||
queryRes = await fetchCustomizeResume(props.jobId)
|
||
if (queryRes.code === 0 && queryRes.data) {
|
||
fillCustomResumeData(queryRes.data)
|
||
currentStep.value = 4
|
||
} else {
|
||
ElMessage.error('获取定制简历数据失败')
|
||
}
|
||
} catch (e) {
|
||
console.error('[JobResumeCustomDialog] 定制简历流程失败', e)
|
||
ElMessage.error('定制简历失败,请稍后重试')
|
||
} finally {
|
||
loadingInstance.close()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将定制简历接口返回的数据填充到简历模板
|
||
*/
|
||
function fillCustomResumeData(data: CustomizeResumeData) {
|
||
// 保存原始数据供编辑面板使用
|
||
customResumeRawData.value = JSON.parse(JSON.stringify(data))
|
||
const r = data.resume || {}
|
||
resumeTemplateData.value = {
|
||
name: r.name || '未填写姓名',
|
||
email: r.email || '',
|
||
mobileNumber: r.mobileNumber || '',
|
||
wechatNumber: r.wechatNumber || '',
|
||
summary: r.summary || '',
|
||
educations: (data.education || []).map(e => ({
|
||
school: e.school || '',
|
||
major: e.major || '',
|
||
degree: degreeToNumber(e.degree),
|
||
startDate: e.startDate || '',
|
||
endDate: e.endDate || '',
|
||
description: (e.description || []).map(d => ({ id: d.id, text: d.text || '' })),
|
||
})),
|
||
workExperiences: (data.work || []).map(w => ({
|
||
companyName: w.companyName || '',
|
||
position: w.position || '',
|
||
startDate: w.startDate || '',
|
||
endDate: w.endDate || '',
|
||
description: (w.description || []).map(d => ({ id: d.id, text: d.text || '' })),
|
||
})),
|
||
internships: (data.internship || []).map(i => ({
|
||
companyName: i.companyName || '',
|
||
position: i.position || '',
|
||
startDate: i.startDate || '',
|
||
endDate: i.endDate || '',
|
||
description: (i.description || []).map(d => ({ id: d.id, text: d.text || '' })),
|
||
})),
|
||
projects: (data.project || []).map(p => ({
|
||
projectName: p.projectName || '',
|
||
companyName: p.companyName || '',
|
||
role: p.role || '',
|
||
startDate: p.startDate || '',
|
||
endDate: p.endDate || '',
|
||
description: (p.description || []).map(d => ({ id: d.id, text: d.text || '' })),
|
||
})),
|
||
competitions: (data.competition || []).map(c => ({
|
||
competitionName: c.competitionName || '',
|
||
award: c.award || '',
|
||
awardDate: c.awardDate || '',
|
||
description: (c.description || []).map(d => ({ id: d.id, text: d.text || '' })),
|
||
})),
|
||
skills: r.skills || [],
|
||
certificates: r.certificates || [],
|
||
}
|
||
}
|
||
|
||
/** 学历文字转数字(接口返回中文,模板需要数字) */
|
||
function degreeToNumber(degree?: string): number {
|
||
const map: Record<string, number> = { '大专': 1, '本科': 2, '硕士': 3, '博士': 4 }
|
||
return map[degree || ''] || 2
|
||
}
|
||
|
||
/**
|
||
* 编辑面板数据更新回调 — 同步更新简历模板预览
|
||
* @param data 编辑面板传回的最新数据
|
||
*/
|
||
function onEditPanelUpdate(data: CustomizeResumeData) {
|
||
fillCustomResumeData(data)
|
||
}
|
||
|
||
/** 关闭弹窗并重置步骤 */
|
||
function handleClose() {
|
||
currentStep.value = 1
|
||
showResumeDropdown.value = false
|
||
showDownloadMenu.value = false
|
||
// 重置AI对话和差异对比状态
|
||
aiMessages.value = []
|
||
aiInputText.value = ''
|
||
aiLoading.value = false
|
||
isShowDiff.value = false
|
||
emit('update:modelValue', false)
|
||
}
|
||
|
||
/** 不优化,直接投递 */
|
||
function handleSkip() {
|
||
handleClose()
|
||
emit('skip')
|
||
}
|
||
|
||
// ==================== 定制简历选项(步骤三) ====================
|
||
|
||
/** 优化部分选项 */
|
||
interface OptimizeSection {
|
||
key: string
|
||
label: string
|
||
checked: boolean
|
||
tooltip?: string
|
||
}
|
||
|
||
/** 技能关键词选项 */
|
||
interface SkillOption {
|
||
name: string
|
||
checked: boolean
|
||
}
|
||
|
||
/** 左侧:可优化的简历部分 */
|
||
const optimizeSections = ref<OptimizeSection[]>([
|
||
{ key: 'summary', label: '个人概述', checked: false },
|
||
{ key: 'skills', label: '技能', checked: false, tooltip: '我们将把您勾选的技能补充进简历的技能模块。这对于简历能否通过ATS关键词筛选至关重要。' },
|
||
{ key: 'experience', label: '工作经验', checked: false, tooltip: '我们将把您选择的技能融入工作经历中,并对关键词进行润色,最大程度提升简历与岗位的匹配度。' },
|
||
])
|
||
|
||
/** 右侧:可新增的技能关键词 */
|
||
const newSkillOptions = ref<SkillOption[]>([])
|
||
|
||
/** 根据缺失技能初始化技能选项 */
|
||
function initSkillOptions() {
|
||
newSkillOptions.value = missingSkills.value.map(skill => ({
|
||
name: skill,
|
||
checked: false,
|
||
}))
|
||
}
|
||
|
||
/** 获取已勾选的优化部分 key 数组(对应 ["summary", "skills", "experience"]) */
|
||
const selectedSectionKeys = computed(() =>
|
||
optimizeSections.value.filter(s => s.checked).map(s => s.key)
|
||
)
|
||
|
||
/** 获取已勾选的新增技能名称数组 */
|
||
const selectedNewSkills = computed(() =>
|
||
newSkillOptions.value.filter(s => s.checked).map(s => s.name)
|
||
)
|
||
|
||
// ==================== 简历选择(步骤二) ====================
|
||
|
||
/** 简历列表(从接口获取) */
|
||
const resumeList = ref<ResumeOption[]>([])
|
||
|
||
/** 当前选中的简历 */
|
||
const selectedResume = ref<ResumeOption>({ id: '', name: '', targetJob: '' })
|
||
|
||
/** 简历下拉是否展开 */
|
||
const showResumeDropdown = ref(false)
|
||
|
||
/** 加载简历列表(调用 /resume/list 接口) */
|
||
async function loadResumeList() {
|
||
try {
|
||
const res = await fetchResumeList()
|
||
if (res.code === '0' && res.data) {
|
||
resumeList.value = res.data.map((item: ResumeListItem) => ({
|
||
id: item.id || '',
|
||
name: item.resumeName || '未命名简历',
|
||
targetJob: item.targetPosition || '',
|
||
}))
|
||
}
|
||
} catch (e) {
|
||
console.error('[JobResumeCustomDialog] 加载简历列表失败', e)
|
||
}
|
||
}
|
||
|
||
/** 初始化简历选择(设置默认选中项) */
|
||
function initResumeSelection() {
|
||
const defaultResume = props.jobInfo.defaultResume
|
||
if (defaultResume) {
|
||
// 用 skill-gap 接口返回的简历作为默认选中
|
||
const defaultOption: ResumeOption = {
|
||
id: defaultResume.resumeId,
|
||
name: defaultResume.resumeName,
|
||
targetJob: defaultResume.targetPosition || '',
|
||
}
|
||
selectedResume.value = defaultOption
|
||
// 如果列表中没有这个简历,插入到列表头部
|
||
if (!resumeList.value.find(r => r.id === defaultOption.id)) {
|
||
resumeList.value.unshift(defaultOption)
|
||
}
|
||
} else if (resumeList.value.length > 0) {
|
||
selectedResume.value = resumeList.value[0]
|
||
}
|
||
}
|
||
|
||
/** 监听弹窗打开,加载简历列表 */
|
||
watch(() => props.modelValue, async (val) => {
|
||
if (val) {
|
||
await loadResumeList()
|
||
initResumeSelection()
|
||
}
|
||
})
|
||
|
||
/** 切换简历下拉 */
|
||
function toggleResumeDropdown() {
|
||
showResumeDropdown.value = !showResumeDropdown.value
|
||
}
|
||
|
||
/** 选择简历 */
|
||
function selectResume(r: ResumeOption) {
|
||
selectedResume.value = r
|
||
showResumeDropdown.value = false
|
||
}
|
||
|
||
// ==================== 步骤四:预览相关 ====================
|
||
|
||
/** 简历模板组件引用 */
|
||
const resumeTemplateRef = ref()
|
||
|
||
/** 简历模板数据 */
|
||
const resumeTemplateData = ref<ResumeTemplateData>({
|
||
name: '',
|
||
email: '',
|
||
mobileNumber: '',
|
||
wechatNumber: '',
|
||
summary: '',
|
||
educations: [],
|
||
workExperiences: [],
|
||
internships: [],
|
||
projects: [],
|
||
competitions: [],
|
||
skills: [],
|
||
certificates: [],
|
||
})
|
||
|
||
/** 定制简历原始数据(传给编辑面板组件) */
|
||
const customResumeRawData = ref<CustomizeResumeData>({
|
||
resume: {},
|
||
})
|
||
|
||
/** 当前预览右侧tab:ai-AI帮写 / edit-编辑 */
|
||
const previewTab = ref<'ai' | 'edit'>('ai')
|
||
|
||
/** AI优化结果列表(根据步骤三勾选的优化项动态生成) */
|
||
const aiOptimizeResults = computed<string[]>(() => {
|
||
const results: string[] = []
|
||
|
||
// 根据「1.选择你要优化的部分」勾选情况生成说明
|
||
const checkedSections = optimizeSections.value.filter(s => s.checked)
|
||
checkedSections.forEach(section => {
|
||
switch (section.key) {
|
||
case 'summary':
|
||
results.push('优化了个人概述,使其更贴合目标岗位要求')
|
||
break
|
||
case 'skills':
|
||
results.push('优化了技能模块,补充了与岗位匹配的关键技能词')
|
||
break
|
||
case 'experience':
|
||
results.push('润色了工作经历描述,融入岗位相关关键词以提升匹配度')
|
||
break
|
||
}
|
||
})
|
||
|
||
// 兜底:如果什么都没勾选,给一个默认提示
|
||
if (results.length === 0) {
|
||
results.push('已根据岗位要求对简历进行整体优化')
|
||
}
|
||
|
||
return results
|
||
})
|
||
|
||
/**
|
||
* 根据勾选的优化部分数量计算优化后的匹配评分
|
||
* 0个勾选:3~5之间随机(一位小数)
|
||
* 1个勾选:6~7之间随机
|
||
* 2个勾选:8~9之间随机
|
||
* 3个勾选:9~10之间随机,50%概率直接为10.0
|
||
*/
|
||
const optimizedMatchScore = computed(() => {
|
||
const checkedCount = optimizeSections.value.filter(s => s.checked).length
|
||
let min: number, max: number
|
||
if (checkedCount === 0) {
|
||
min = 3; max = 5
|
||
} else if (checkedCount === 1) {
|
||
min = 6; max = 7
|
||
} else if (checkedCount === 2) {
|
||
min = 8; max = 9
|
||
} else {
|
||
// 3个勾选:50%概率直接10.0
|
||
if (Math.random() < 0.2) return 10.0
|
||
min = 9; max = 10
|
||
}
|
||
// 生成 [min, max] 之间的一位小数随机值
|
||
const raw = min + Math.random() * (max - min)
|
||
return Math.round(raw * 10) / 10
|
||
})
|
||
|
||
/** 缓存优化后评分(避免computed每次重新随机) */
|
||
const cachedOptimizedScore = ref<number>(0)
|
||
|
||
/** 进入步骤4时缓存一次评分 */
|
||
watch(currentStep, (val) => {
|
||
if (val === 4) {
|
||
cachedOptimizedScore.value = optimizedMatchScore.value
|
||
}
|
||
})
|
||
|
||
/** AI快捷操作按钮 */
|
||
const aiQuickActions = ref<string[]>([
|
||
'精简一下第一段工作经历',
|
||
'帮我强化一下简历里面的量化成果',
|
||
'删掉和这个岗位不相关的技能',
|
||
])
|
||
|
||
/** AI聊天消息列表(界面展示用) */
|
||
const aiMessages = ref<AiChatMsg[]>([])
|
||
|
||
/** AI输入框内容 */
|
||
const aiInputText = ref('')
|
||
|
||
/** AI消息区域DOM引用 */
|
||
const aiMessagesRef = ref<HTMLElement>()
|
||
|
||
/** AI是否正在请求中 */
|
||
const aiLoading = ref(false)
|
||
|
||
/** 是否显示差异对比模式 */
|
||
const isShowDiff = ref(false)
|
||
|
||
/** 旧简历模板数据(AI修改前的快照,用于差异对比) */
|
||
const oldResumeTemplateData = ref<ResumeTemplateData>({
|
||
name: '',
|
||
email: '',
|
||
mobileNumber: '',
|
||
wechatNumber: '',
|
||
summary: '',
|
||
educations: [],
|
||
workExperiences: [],
|
||
internships: [],
|
||
projects: [],
|
||
competitions: [],
|
||
skills: [],
|
||
certificates: [],
|
||
})
|
||
|
||
/**
|
||
* 构建发送给接口的chatHistory格式
|
||
* 将界面展示的消息列表转换为接口需要的格式
|
||
*/
|
||
function buildChatHistory(): AiEditChatMessage[] {
|
||
return aiMessages.value.map(msg => ({
|
||
role: msg.role,
|
||
content: msg.content,
|
||
}))
|
||
}
|
||
|
||
/**
|
||
* 滚动AI消息区域到底部
|
||
*/
|
||
function scrollAiMessagesToBottom() {
|
||
nextTick(() => {
|
||
if (aiMessagesRef.value) {
|
||
aiMessagesRef.value.scrollTop = aiMessagesRef.value.scrollHeight
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 发送AI消息 — 调用AI对话编辑简历接口
|
||
* @param text 用户输入的消息文本
|
||
*/
|
||
async function sendAiMessage(text: string) {
|
||
if (!text.trim() || aiLoading.value) return
|
||
|
||
const userMessage = text.trim()
|
||
// 添加用户消息到列表
|
||
aiMessages.value.push({ role: 'user', content: userMessage })
|
||
// 清空输入框
|
||
aiInputText.value = ''
|
||
// 滚动到底部
|
||
scrollAiMessagesToBottom()
|
||
|
||
// 开始请求
|
||
aiLoading.value = true
|
||
try {
|
||
const res = await aiEditResume({
|
||
jobId: props.jobId,
|
||
instruction: userMessage,
|
||
chatHistory: buildChatHistory().slice(0, -1), // 不包含刚发送的这条
|
||
})
|
||
|
||
if (res.code === 0 && res.data) {
|
||
const { type, message } = res.data
|
||
|
||
// 添加AI助手回复到消息列表
|
||
aiMessages.value.push({ role: 'assistant', content: message, canRollback: type === 'updated', rollbackStatus: 'idle' })
|
||
scrollAiMessagesToBottom()
|
||
|
||
if (type === 'updated') {
|
||
// 简历已更新:深拷贝当前简历数据作为旧数据快照
|
||
oldResumeTemplateData.value = JSON.parse(JSON.stringify(resumeTemplateData.value))
|
||
|
||
// 重新查询定制简历数据来刷新简历预览
|
||
const queryRes = await fetchCustomizeResume(props.jobId)
|
||
if (queryRes.code === 0 && queryRes.data) {
|
||
fillCustomResumeData(queryRes.data)
|
||
// 开启差异对比模式
|
||
isShowDiff.value = true
|
||
}
|
||
}
|
||
// type === 'message' 时只显示对话,不做额外操作
|
||
} else {
|
||
// 接口返回异常
|
||
aiMessages.value.push({ role: 'assistant', content: '抱歉,请求失败了,请稍后重试。' })
|
||
scrollAiMessagesToBottom()
|
||
}
|
||
} catch (e) {
|
||
console.error('[JobResumeCustomDialog] AI对话请求失败', e)
|
||
aiMessages.value.push({ role: 'assistant', content: '网络异常,请稍后重试。' })
|
||
scrollAiMessagesToBottom()
|
||
} finally {
|
||
aiLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 下载菜单是否展开 */
|
||
const showDownloadMenu = ref(false)
|
||
|
||
/** 撤销确认弹窗是否显示 */
|
||
const showRollbackConfirm = ref(false)
|
||
|
||
/** 当前要撤销的消息索引 */
|
||
const rollbackMsgIndex = ref(-1)
|
||
|
||
/**
|
||
* 判断该消息是否为消息列表的最后一条
|
||
* 撤销按钮只在最后一条消息恰好是AI修改简历的回答时才显示
|
||
* @param msgIndex 消息在列表中的索引
|
||
*/
|
||
function isLastMessage(msgIndex: number): boolean {
|
||
return msgIndex === aiMessages.value.length - 1
|
||
}
|
||
|
||
/**
|
||
* 点击撤销修改按钮 — 弹出确认弹窗
|
||
* @param msgIndex 消息在列表中的索引
|
||
*/
|
||
function handleRollbackClick(msgIndex: number) {
|
||
rollbackMsgIndex.value = msgIndex
|
||
showRollbackConfirm.value = true
|
||
}
|
||
|
||
/**
|
||
* 确认撤销修改 — 调用 rollback 接口并刷新简历数据
|
||
*/
|
||
async function confirmRollback() {
|
||
showRollbackConfirm.value = false
|
||
const idx = rollbackMsgIndex.value
|
||
if (idx < 0) return
|
||
|
||
try {
|
||
const res = await rollbackCustomizeResume(props.jobId)
|
||
if (res.code === 0) {
|
||
// 标记该消息为已撤销
|
||
aiMessages.value[idx].rollbackStatus = 'done'
|
||
// 关闭差异对比模式
|
||
isShowDiff.value = false
|
||
// 重新查询简历数据刷新预览
|
||
const queryRes = await fetchCustomizeResume(props.jobId)
|
||
if (queryRes.code === 0 && queryRes.data) {
|
||
fillCustomResumeData(queryRes.data)
|
||
}
|
||
} else {
|
||
ElMessage.error('撤销失败,请稍后重试')
|
||
}
|
||
} catch (e) {
|
||
console.error('[JobResumeCustomDialog] 撤销修改失败', e)
|
||
ElMessage.error('撤销失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
/** 取消撤销 */
|
||
function cancelRollback() {
|
||
showRollbackConfirm.value = false
|
||
rollbackMsgIndex.value = -1
|
||
}
|
||
|
||
/** 切换下载菜单 */
|
||
function toggleDownloadMenu() {
|
||
showDownloadMenu.value = !showDownloadMenu.value
|
||
}
|
||
|
||
/** 处理下载(PDF/Word) */
|
||
async function handleDownload(type: 'pdf' | 'word') {
|
||
showDownloadMenu.value = false
|
||
|
||
const element = resumeTemplateRef.value?.resumeRef
|
||
if (!element) {
|
||
console.error('[下载简历] 无法获取简历模板DOM')
|
||
return
|
||
}
|
||
|
||
const fileName = (resumeTemplateData.value.name || '简历') + '_定制简历'
|
||
|
||
try {
|
||
if (type === 'pdf') {
|
||
await exportResumePdf(element, fileName)
|
||
} else {
|
||
exportResumeWord(element, fileName)
|
||
}
|
||
} catch (err) {
|
||
console.error('[下载简历] 导出失败', err)
|
||
}
|
||
}
|
||
|
||
/** 立即去投递 */
|
||
function handleSubmit() {
|
||
handleClose()
|
||
emit('submit')
|
||
}
|
||
</script>
|