简历优化和岗位简历

This commit is contained in:
2026-04-13 21:34:40 +08:00
parent a39835be01
commit 1c91818494
32 changed files with 3850 additions and 320 deletions
+446 -109
View File
@@ -10,7 +10,7 @@
<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 class="job-resume-custom-dialog__tip-bar"><span>你与该岗位的匹配度较低简历可能无法通过机筛</span></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">
@@ -52,9 +52,9 @@
<h2 class="job-resume-custom-dialog__drawer-title">生成你的岗位专属简历</h2>
</div>
<!-- 返回按钮步骤四显示 -->
<button v-if="currentStep === 4" class="job-resume-custom-dialog__back-btn" @click="goToStep(3)">返回</button>
<button v-if="currentStep === 4" class="job-resume-custom-dialog__back-btn mt10" @click="goToStep(3)">返回</button>
<!-- 步骤指示器 -->
<div class="job-resume-custom-dialog__steps">
<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>
@@ -71,8 +71,8 @@
<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">你的简历与该岗位的匹配度较低</h3>
<div class="job-resume-custom-dialog__gap-warn">
<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>
@@ -131,9 +131,9 @@
<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">
<!-- <div class="job-resume-custom-dialog__gap-cell">
<span class="job-resume-custom-dialog__gap-value">{{ selectedResume.targetJob || '—' }}</span>
</div>
</div> -->
</div>
<!-- 第三行岗位关键词 -->
<div class="job-resume-custom-dialog__gap-row">
@@ -195,9 +195,14 @@
<!-- 步骤四预览 -->
<template v-if="currentStep === 4">
<div class="job-resume-custom-dialog__preview">
<!-- 左侧简历模板预览 -->
<!-- 左侧简历模板预览支持差异对比模式 -->
<div class="job-resume-custom-dialog__preview-left">
<JobResumeTemplate :resumeData="resumeTemplateData" ref="resumeTemplateRef" />
<JobResumeTemplate
:resumeData="resumeTemplateData"
:showDiff="isShowDiff"
:oldResumeData="oldResumeTemplateData"
ref="resumeTemplateRef"
/>
</div>
<!-- 右侧AI帮写 / 编辑 tab -->
<div class="job-resume-custom-dialog__preview-right">
@@ -252,10 +257,37 @@
<div
v-for="(msg, i) in aiMessages"
:key="i"
class="job-resume-custom-dialog__ai-msg"
:class="msg.role === 'ai' ? 'job-resume-custom-dialog__ai-msg--ai' : 'job-resume-custom-dialog__ai-msg--user'"
class="job-resume-custom-dialog__ai-msg-wrap"
>
<div class="job-resume-custom-dialog__ai-msg-bubble">{{ msg.content }}</div>
<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>
<!-- 撤销修改气泡 type=updated assistant 消息显示 -->
<div v-if="msg.canRollback" class="job-resume-custom-dialog__ai-rollback">
<button
v-if="msg.rollbackStatus !== 'done'"
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>
<span v-else 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>
</div>
<!-- AI正在回复的加载指示器 -->
<div v-if="aiLoading" class="job-resume-custom-dialog__ai-msg job-resume-custom-dialog__ai-msg--ai">
<div class="job-resume-custom-dialog__ai-msg-bubble job-resume-custom-dialog__ai-msg-bubble--loading">AI正在思考中...</div>
</div>
</div>
<!-- AI输入框 -->
@@ -273,9 +305,9 @@
</button>
</div>
</div>
<!-- 编辑内容占位 -->
<!-- 编辑内容 折叠手风琴式编辑面板 -->
<div v-if="previewTab === 'edit'" class="job-resume-custom-dialog__preview-edit">
<div class="job-resume-custom-dialog__placeholder">编辑功能待开发</div>
<JobResumeCustomEditPanel :resumeData="customResumeRawData" @update="onEditPanelUpdate" />
</div>
</div>
</div>
@@ -299,15 +331,30 @@
<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 } from 'vue'
import { ref, computed, nextTick, watch } from 'vue'
import html2pdf from 'html2pdf.js'
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
import JobResumeCustomEditPanel from '@/components/JobResumeCustomEditPanel.vue'
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
import { fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
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'
// ==================== 类型定义 ====================
@@ -328,12 +375,20 @@ interface JobInfo {
missingSkills: string[]
keywords: string[]
sourceUrl: string
/** 默认简历信息(来自 skill-gap 接口) */
defaultResume: { resumeId: string; resumeName: string; targetPosition: string } | null
}
/** AI聊天消息 */
/** AI聊天消息(用于界面展示) */
interface AiChatMsg {
role: 'ai' | 'user'
/** 角色:user-用户 assistant-AI助手 */
role: 'user' | 'assistant'
/** 消息内容 */
content: string
/** 是否可以撤销(仅 type=updated 的 assistant 消息有此标记) */
canRollback?: boolean
/** 撤销状态:idle-未操作 done-已撤销 */
rollbackStatus?: 'idle' | 'done'
}
// ==================== Props & Emits ====================
@@ -343,6 +398,8 @@ const props = defineProps<{
modelValue: boolean
/** 岗位信息 */
jobInfo: JobInfo
/** 岗位 ID(字符串,避免大整数精度丢失) */
jobId: string
}>()
const emit = defineEmits<{
@@ -359,35 +416,163 @@ const currentStep = ref(1)
/** 缺少的技能列表 */
const missingSkills = computed(() => props.jobInfo.missingSkills || [])
/** 匹配度等级文案 */
/** 匹配度等级文案6分为高低分界线) */
const matchLevelText = computed(() => {
const score = props.jobInfo.matchScore
if (score >= 8) return '高匹配度'
if (score >= 5) return '中匹配度'
if (score >= 6) return '高匹配度'
return '低匹配度'
})
/** 是否为低匹配度(低于6分) */
const isLowMatch = computed(() => props.jobInfo.matchScore < 6)
/** 跳转到指定步骤 */
function goToStep(step: number) {
if (step === 3) initSkillOptions()
if (step === 4) loadResumeData()
currentStep.value = step
if (step === 4) fetchAndLoadCustomResume()
else currentStep.value = step
}
/** 抽屉模式下一步 */
function handleDrawerNext() {
if (currentStep.value < 4) {
if (currentStep.value === 2) initSkillOptions()
if (currentStep.value === 3) loadResumeData()
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()
// 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()
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)
}
@@ -431,20 +616,71 @@ function initSkillOptions() {
}))
}
/** 获取已勾选的优化部分 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[]>([
{ id: '1', name: '李华_产品经理', targetJob: '电商产品经理' },
{ id: '2', name: '李华_数据分析', targetJob: '数据分析师' },
])
/** 简历列表(从接口获取 */
const resumeList = ref<ResumeOption[]>([])
/** 当前选中的简历 */
const selectedResume = ref<ResumeOption>(resumeList.value[0])
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
@@ -477,79 +713,41 @@ const resumeTemplateData = ref<ResumeTemplateData>({
certificates: [],
})
/** 从接口加载个人资料并组装简历数据 */
async function loadResumeData() {
try {
// 并行请求所有个人资料数据
const [profileRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
fetchProfile(),
fetchEducation(),
fetchWork(),
fetchInternship(),
fetchProject(),
fetchCompetition(),
])
// 组装简历模板数据
const profile = profileRes.code === '0' ? profileRes.data : null
resumeTemplateData.value = {
name: profile?.name || '未填写姓名',
email: profile?.email || '',
mobileNumber: profile?.mobileNumber || '',
wechatNumber: profile?.wechatNumber || '',
summary: '', // 个人概述字段,后续由AI生成或用户编辑
educations: eduRes.code === '0' && eduRes.data ? eduRes.data.map(e => ({
school: e.school || '',
major: e.major || '',
degree: e.degree || 2,
startDate: e.startDate || '',
endDate: e.endDate || '',
description: e.description,
})) : [],
workExperiences: workRes.code === '0' && workRes.data ? workRes.data.map(w => ({
companyName: w.companyName || '',
position: w.position || '',
startDate: w.startDate || '',
endDate: w.endDate || '',
description: w.description,
})) : [],
internships: internRes.code === '0' && internRes.data ? internRes.data.map(i => ({
companyName: i.companyName || '',
position: i.position || '',
startDate: i.startDate || '',
endDate: i.endDate || '',
description: i.description,
})) : [],
projects: projRes.code === '0' && projRes.data ? projRes.data.map(p => ({
projectName: p.projectName || '',
companyName: p.companyName || '',
role: p.role || '',
startDate: p.startDate || '',
endDate: p.endDate || '',
description: p.description,
})) : [],
competitions: compRes.code === '0' && compRes.data ? compRes.data.map(c => ({
competitionName: c.competitionName || '',
award: c.award || '',
awardDate: c.awardDate || '',
description: c.description,
})) : [],
skills: profile?.skills || [],
certificates: profile?.certificates || [],
}
} catch (err) {
console.error('[JobResumeCustomDialog] 加载简历数据失败', err)
}
}
/** 定制简历原始数据(传给编辑面板组件) */
const customResumeRawData = ref<CustomizeResumeData>({
resume: {},
})
/** 当前预览右侧tabai-AI帮写 / edit-编辑 */
const previewTab = ref<'ai' | 'edit'>('ai')
/** AI优化结果列表(模拟数据 */
const aiOptimizeResults = ref<string[]>([
'增加了个人概述',
'优化了5段经历描述',
])
/** 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
})
/** AI快捷操作按钮 */
const aiQuickActions = ref<string[]>([
@@ -558,7 +756,7 @@ const aiQuickActions = ref<string[]>([
'删掉和这个岗位不相关的技能',
])
/** AI聊天消息列表 */
/** AI聊天消息列表(界面展示用) */
const aiMessages = ref<AiChatMsg[]>([])
/** AI输入框内容 */
@@ -567,12 +765,43 @@ const aiInputText = ref('')
/** AI消息区域DOM引用 */
const aiMessagesRef = ref<HTMLElement>()
/** 发送AI消息 */
function sendAiMessage(text: string) {
if (!text.trim()) return
aiMessages.value.push({ role: 'user', content: text.trim() })
aiInputText.value = ''
// TODO: 接入AI聊天接口,获取AI回复
/** 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
@@ -580,9 +809,117 @@ function sendAiMessage(text: string) {
})
}
/**
* 发送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()
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)
/**
* 点击撤销修改按钮 — 弹出确认弹窗
* @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()
if (res.code === 0) {
// 标记该消息为已撤销
aiMessages.value[idx].rollbackStatus = 'done'
// 关闭差异对比模式
isShowDiff.value = false
// 重新查询简历数据刷新预览
const queryRes = await fetchCustomizeResume()
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