Files
offerpai_web/src/views/ResumeDetail.vue
T

1241 lines
52 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="resume-detail dflex">
<SideNav />
<div class="resume-detail__content">
<!-- 个人资料编辑抽屉 -->
<ProfileEditDrawer
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
:use-education-text-value="true"
@save="handleSaveEdit"
/>
<!-- 简历分析报告抽屉 -->
<ResumeAnalysisReportDrawer
v-model="showReportDrawer"
:report="diagnosisReport"
:issues="diagnosisIssues"
@optimize="handleClose_report"
/>
<!-- 简历评估问题修复抽屉 -->
<ResumeIssueFixDrawer
v-model="showFixDrawer"
:module-type="fixModuleType"
:record-id="fixRecordId"
:original-content="fixOriginalContent"
:issue="fixIssue"
@submit="handleFixSubmit"
/>
<!-- 导出简历弹窗组件 -->
<ResumeExportDialog
v-model="exportDialogVisible"
:resume-id="resumeId"
:resume-name="resumeMain.resumeName || '简历'"
/>
<!-- 编辑简历名称弹窗 -->
<ResumeEditNameDialog
v-model="editNameVisible"
:resume-id="resumeId"
:resume-name="resumeMain.resumeName || ''"
:target-position="resumeMain.targetPosition || ''"
@saved="loadResumeDetail"
/>
<!-- 顶部标题 -->
<h2 class="resume-detail__page-title">我的简历</h2>
<!-- 顶部操作栏 -->
<div class="resume-detail__toolbar">
<div class="resume-detail__toolbar-left">
<button class="resume-detail__back-btn" @click="goBack" aria-label="返回">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__back-icon">
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<span class="resume-detail__tab-name">{{ resumeMain.resumeName || '未命名简历' }}</span>
</div>
<div class="resume-detail__toolbar-right">
<button class="resume-detail__tool-btn" @click="handleEdit">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
编辑简历信息
</button>
<button class="resume-detail__tool-btn" @click="handleExport">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
<path d="M3 10v3h10v-3M8 2v8M5 5l3-3 3 3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
导出
</button>
<button class="resume-detail__tool-btn resume-detail__tool-btn--danger" @click="handleDelete">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
<path d="M4 4h8l-.5 9H4.5L4 4zM6 4V2.5h4V4M2.5 4h11" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
删除
</button>
</div>
</div>
<!-- 简历评分区域 -->
<div class="resume-detail__score-bar">
<div class="resume-detail__score-left">
<!-- 评级 -->
<span v-if="hasDiagnosis" class="resume-detail__score-avatar">
{{ diagnosisReport.grade }}
</span>
<!-- 有诊断报告时显示评级和查看链接 -->
<template v-if="hasDiagnosis">
<span class="resume-detail__score-badge">{{ gradeLabel }}</span>
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告 ></button>
</template>
</div>
<div class="resume-detail__score-right">
<!-- 有诊断报告时显示三项计数和重新诊断按钮 -->
<template v-if="hasDiagnosis">
<div class="resume-detail__score-item">
<!-- issues status===0 的子项汇总report 数据修复后未更新暂不使用 diagnosisReport.urgentTotal -->
<span class="resume-detail__score-num">{{ issuesUrgentTotal }}</span>
<span class="resume-detail__score-label">紧急修复</span>
</div>
<div class="resume-detail__score-item">
<!-- issues status===0 的子项汇总report 数据修复后未更新暂不使用 diagnosisReport.importantTotal -->
<span class="resume-detail__score-num">{{ issuesImportantTotal }}</span>
<span class="resume-detail__score-label">重点优化</span>
</div>
<div class="resume-detail__score-item">
<!-- issues status===0 的子项汇总report 数据修复后未更新暂不使用 diagnosisReport.expressionTotal -->
<span class="resume-detail__score-num">{{ issuesExpressionTotal }}</span>
<span class="resume-detail__score-label">表达提升</span>
</div>
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
</template>
<!-- 没有诊断报告时显示开始诊断按钮 -->
<template v-else>
<button class="resume-detail__diagnose-btn resume-detail__diagnose-btn--start" @click="handleDiagnose">开始诊断</button>
</template>
</div>
</div>
<!-- 简历主体内容 -->
<div class="resume-detail__body">
<!-- 个人信息卡片 -->
<div class="resume-detail__card">
<div class="resume-detail__card-header">
<div>
<h3 class="resume-detail__user-name">{{ resumeMain.name || '未填写姓名' }}</h3>
<p class="resume-detail__user-title">{{ resumeMain.targetPosition || '' }}</p>
</div>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('info')" aria-label="编辑个人信息">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="resume-detail__contact">
<!-- 邮箱 -->
<span v-if="resumeMain.email" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M2 5.5l6 4 6-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ resumeMain.email }}
</span>
<!-- 手机号 -->
<span v-if="resumeMain.mobileNumber" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M4 2h2.5l1.5 3-1.5 1.5a8 8 0 003 3L11 8l3 1.5V12a2 2 0 01-2 2C6.5 14 2 9.5 2 4a2 2 0 012-2z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
{{ resumeMain.mobileNumber }}
</span>
<!-- 所在城市 -->
<span v-if="resumeMain.city" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
</svg>
{{ resumeMain.city }}
</span>
<!-- 微信号 -->
<span v-if="resumeMain.wechatNumber" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M2 8a6 6 0 1112 0 6 6 0 01-12 0z" stroke="currentColor" stroke-width="1.2"/>
<path d="M5.5 7.5c.5-1 1.5-1.5 2.5-1.5s2 .5 2.5 1.5M6 10s.8 1 2 1 2-1 2-1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ resumeMain.wechatNumber }}
</span>
</div>
</div>
<!-- 个人概述 -->
<div v-if="resumeMain.summary" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">个人概述</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('summary')" aria-label="编辑个人概述">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<p class="resume-detail__summary-text">{{ resumeMain.summary }}</p>
<!-- 个人概述的问题操作区仅诊断数据中有 summary 类型且 status===0 时显示 -->
<div v-for="issue in getIssuesByModule('summary')" :key="issue.id" v-show="issue.status === 0" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(issue)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('summary', resumeId) === t.value }"
@click="setActiveType('summary', resumeId, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('summary')">修复</button>
</div>
</div>
<!-- 作品集链接 -->
<div v-if="resumeMain.portfolioUrl" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">作品集</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('portfolio')" aria-label="编辑作品集">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<a :href="resumeMain.portfolioUrl" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">
{{ resumeMain.portfolioUrl }}
</a>
</div>
<!-- 教育背景经历 -->
<div v-if="educationList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">教育背景</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('education')" aria-label="编辑教育背景">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="edu in educationList" :key="edu.id" class="resume-detail__edu-item">
<div class="resume-detail__edu-degree">{{ edu.degree }} · {{ edu.major }}</div>
<div class="resume-detail__edu-meta">{{ edu.school }} · {{ edu.studyType }} · {{ edu.startDate }} - {{ edu.endDate }}</div>
<div v-if="edu.description?.length" class="resume-detail__desc-list">
<p v-for="p in edu.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段教育经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('education', edu.id!) && getIssueByRecord('education', edu.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('education', edu.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('education', edu.id!) === t.value }"
@click="setActiveType('education', edu.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('education', edu.id)">修复</button>
</div>
</template>
</div>
</div>
<!-- 工作经历 -->
<div v-if="workList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">工作经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('work')" aria-label="编辑工作经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="exp in workList" :key="exp.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ exp.companyName }}</span>
<span v-if="exp.startDate||exp.endDate" class="color-5 fw400 fs12 pt5">{{ exp.startDate }} <span v-if="exp.startDate&&exp.endDate"> - </span> {{ exp.endDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ exp.position }} </div>
<ul v-if="exp.description?.length" class="resume-detail__desc-list pl16">
<li v-for="p in exp.description" :key="p.id" class="resume-detail__desc-item fs12 color-5">{{ p.text }}</li>
</ul>
<!-- 每段工作经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('work', exp.id!) && getIssueByRecord('work', exp.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('work', exp.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('work', exp.id!) === t.value }"
@click="setActiveType('work', exp.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('work', exp.id)">修复</button>
</div>
</template>
</div>
</div>
<!-- 实习经历 -->
<div v-if="internshipList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">实习经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('internship')" aria-label="编辑实习经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="intern in internshipList" :key="intern.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ intern.position }}</span>
<span v-if="intern.startDate||intern.endDate" class="color-5 fw400 fs12 pt5">{{ intern.startDate }} <span v-if="intern.startDate&&intern.endDate"> - </span> {{ intern.endDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ intern.companyName }}</div>
<div v-if="intern.description?.length" class="resume-detail__desc-list">
<p v-for="p in intern.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段实习经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('internship', intern.id!) && getIssueByRecord('internship', intern.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('internship', intern.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('internship', intern.id!) === t.value }"
@click="setActiveType('internship', intern.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('internship', intern.id)">修复</button>
</div>
</template>
</div>
</div>
<!-- 项目经历 -->
<div v-if="projectList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">项目经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('project')" aria-label="编辑项目经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="proj in projectList" :key="proj.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ proj.projectName }}</span>
<span v-if="proj.startDate||proj.endDate" class="color-5 fw400 fs12 pt5">{{ proj.startDate }} <span v-if="proj.startDate&&proj.endDate"> - </span> {{ proj.endDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ proj.companyName }} · {{ proj.role }}</div>
<div v-if="proj.description?.length" class="resume-detail__desc-list">
<p v-for="p in proj.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段项目经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('project', proj.id!) && getIssueByRecord('project', proj.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('project', proj.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('project', proj.id!) === t.value }"
@click="setActiveType('project', proj.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('project', proj.id)">修复</button>
</div>
</template>
</div>
</div>
<!-- 竞赛经历 -->
<div v-if="competitionList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">竞赛经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('competition')" aria-label="编辑竞赛经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="comp in competitionList" :key="comp.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ comp.competitionName }}</span>
<span v-if="comp.awardDate" class="color-5 fw400 fs12 pt5">{{ comp.awardDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ comp.award }}</div>
<div v-if="comp.description?.length" class="resume-detail__desc-list">
<p v-for="p in comp.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段竞赛经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('competition', comp.id!) && getIssueByRecord('competition', comp.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('competition', comp.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('competition', comp.id!) === t.value }"
@click="setActiveType('competition', comp.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('competition', comp.id)">修复</button>
</div>
</template>
</div>
</div>
<!-- 技能 -->
<div v-if="resumeMain.skills?.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">技能</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('skills')" aria-label="编辑技能">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="resume-detail__skills">
<span v-for="(skill, i) in resumeMain.skills" :key="i" class="resume-detail__skill-tag">
{{ skill }}
</span>
</div>
</div>
<!-- 证书 -->
<div v-if="resumeMain.certificates?.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">证书</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('certificate')" aria-label="编辑证书">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="resume-detail__skills">
<span v-for="(cert, i) in resumeMain.certificates" :key="i" class="resume-detail__skill-tag">
{{ cert }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
import ResumeAnalysisReportDrawer from '@/components/ResumeAnalysisReportDrawer.vue'
import ResumeIssueFixDrawer from '@/components/ResumeIssueFixDrawer.vue'
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
import {
fetchResumeMain,
fetchResumeEducation,
fetchResumeWork,
fetchResumeInternship,
fetchResumeProject,
fetchResumeCompetition,
fetchResumeDiagnosis,
triggerResumeDiagnosis,
saveResumeMain,
addResumeEducation,
updateResumeEducation,
deleteResumeEducation,
addResumeWork,
updateResumeWork,
deleteResumeWork,
addResumeInternship,
updateResumeInternship,
deleteResumeInternship,
addResumeProject,
updateResumeProject,
deleteResumeProject,
addResumeCompetition,
updateResumeCompetition,
deleteResumeCompetition,
deleteResume,
type ResumeMainData,
type ResumeEducation,
type ResumeWork,
type ResumeInternship,
type ResumeProject,
type ResumeCompetition,
type DiagnosisReport,
type DiagnosisIssue,
type DescriptionParagraph,
} from '@/api/resume'
// ==================== 诊断数据相关 ====================
/** 问题类型枚举值 */
type IssueType = 'urgent' | 'optimize' | 'expression'
/** 是否已有诊断报告 */
const hasDiagnosis = ref(false)
/** 诊断报告数据 */
const diagnosisReport = ref<DiagnosisReport>({})
/** 诊断问题列表 */
const diagnosisIssues = ref<DiagnosisIssue[]>([])
/** 评级文字映射 */
const gradeLabel = computed(() => {
const map: Record<string, string> = { A: '优秀', B: '良好', C: '一般', D: '待提升' }
return map[diagnosisReport.value.grade || ''] || '未评级'
})
/**
* 从 issues 中 status===0 的子项汇总紧急修复数
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.urgentTotal,等后面接口改了用回来)
*/
const issuesUrgentTotal = computed(() => {
return diagnosisIssues.value
.filter(i => i.status === 0)
.reduce((sum, i) => sum + sumSubCounts(i.urgentIssues), 0)
})
/**
* 从 issues 中 status===0 的子项汇总重点优化数
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.importantTotal,等后面接口改了用回来)
*/
const issuesImportantTotal = computed(() => {
return diagnosisIssues.value
.filter(i => i.status === 0)
.reduce((sum, i) => sum + sumSubCounts(i.importantIssues), 0)
})
/**
* 从 issues 中 status===0 的子项汇总表达提升数
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.expressionTotal,等后面接口改了用回来)
*/
const issuesExpressionTotal = computed(() => {
return diagnosisIssues.value
.filter(i => i.status === 0)
.reduce((sum, i) => sum + sumSubCounts(i.expressionIssues), 0)
})
/** 每个子经历的问题操作区当前选中类型,key 为 "moduleType_recordId" */
const issueActiveTypes = reactive<Record<string, IssueType>>({})
/** 根据模块类型获取该模块的所有 issue */
function getIssuesByModule(moduleType: string): DiagnosisIssue[] {
return diagnosisIssues.value.filter(i => i.moduleType === moduleType)
}
/** 根据模块类型和列表索引获取对应的 issue(用于 ID 不匹配的模块) */
function getIssueByIndex(moduleType: string, index: number): DiagnosisIssue | undefined {
return getIssuesByModule(moduleType)[index]
}
/** 根据模块类型和记录 ID 获取对应的 issue(备用,后端 ID 修复后可恢复使用) */
function getIssueByRecord(moduleType: string, recordId: string): DiagnosisIssue | undefined {
return diagnosisIssues.value.find(i => i.moduleType === moduleType && i.moduleRecordId === recordId)
}
/** 根据模块类型和记录 ID 获取对应的 issue — 当前因后端 ID 不一致,内部走索引匹配 */
// function getIssueByRecord(moduleType: string, recordId: string): DiagnosisIssue | undefined {
// // 个人概述用 resumeId 匹配
// if (moduleType === 'summary') {
// return diagnosisIssues.value.find(i => i.moduleType === 'summary')
// }
// // 其他经历模块:根据列表索引匹配(后端 moduleRecordId 与经历 id 不一致的临时方案)
// const listMap: Record<string, any[]> = {
// education: educationList.value,
// work: workList.value,
// internship: internshipList.value,
// project: projectList.value,
// competition: competitionList.value,
// }
// const list = listMap[moduleType]
// if (!list) return undefined
// const idx = list.findIndex((item: any) => item.id === recordId)
// if (idx === -1) return undefined
// return getIssueByIndex(moduleType, idx)
// }
/** 计算单个 issue 的子类型计数,将 urgentIssues/importantIssues/expressionIssues 的 value 求和 */
function sumSubCounts(obj?: Record<string, number>): number {
if (!obj) return 0
return Object.values(obj).reduce((s, v) => s + v, 0)
}
/** 获取单个 issue 的三类问题计数,用于渲染问题类型按钮 */
function getIssueTypeCounts(issue: DiagnosisIssue) {
const urgent = sumSubCounts(issue.urgentIssues)
const important = sumSubCounts(issue.importantIssues)
const expression = sumSubCounts(issue.expressionIssues)
const result: { label: string; value: IssueType; count: number }[] = []
if (urgent > 0) result.push({ label: '紧急修复', value: 'urgent', count: urgent })
if (important > 0) result.push({ label: '重点优化', value: 'optimize', count: important })
if (expression > 0) result.push({ label: '表达提升', value: 'expression', count: expression })
return result
}
/** 获取某个子经历问题操作区的当前选中类型 */
function getActiveType(moduleType: string, recordId: string): IssueType {
const key = `${moduleType}_${recordId}`
if (!issueActiveTypes[key]) {
// 默认选中第一个有计数的类型
const issue = getIssueByRecord(moduleType, recordId)
if (issue) {
const types = getIssueTypeCounts(issue)
issueActiveTypes[key] = types.length > 0 ? types[0].value : 'urgent'
} else {
issueActiveTypes[key] = 'urgent'
}
}
return issueActiveTypes[key]
}
/** 设置某个子经历问题操作区的选中类型 */
function setActiveType(moduleType: string, recordId: string, type: IssueType) {
issueActiveTypes[`${moduleType}_${recordId}`] = type
}
/** 加载诊断报告数据 */
async function loadDiagnosis() {
try {
const res = await fetchResumeDiagnosis(resumeId)
if (res.code === 0 && res.data) {
hasDiagnosis.value = true
diagnosisReport.value = res.data.report
diagnosisIssues.value = res.data.issues || []
} else {
// data 为 null,没有诊断报告
hasDiagnosis.value = false
diagnosisReport.value = {}
diagnosisIssues.value = []
}
} catch {
console.error('[ResumeDetail] 加载诊断报告失败')
hasDiagnosis.value = false
}
}
// ==================== 路由相关 ====================
const router = useRouter()
const route = useRoute()
/** 当前简历 ID,从路由参数中获取(保持字符串,避免大整数精度丢失) */
const resumeId = route.params.id as string
// ==================== 数据 ====================
/** 简历主表数据 */
const resumeMain = ref<ResumeMainData>({})
/** 教育经历列表 */
const educationList = ref<ResumeEducation[]>([])
/** 工作经历列表 */
const workList = ref<ResumeWork[]>([])
/** 实习经历列表 */
const internshipList = ref<ResumeInternship[]>([])
/** 项目经历列表 */
const projectList = ref<ResumeProject[]>([])
/** 竞赛经历列表 */
const competitionList = ref<ResumeCompetition[]>([])
// ==================== 数据加载 ====================
/** 加载所有简历详情数据 */
async function loadResumeDetail() {
const [mainRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
fetchResumeMain(resumeId),
fetchResumeEducation(resumeId),
fetchResumeWork(resumeId),
fetchResumeInternship(resumeId),
fetchResumeProject(resumeId),
fetchResumeCompetition(resumeId),
])
if (mainRes.code === '0' && mainRes.data) {
resumeMain.value = mainRes.data
}
if (eduRes.code === '0' && eduRes.data) {
educationList.value = eduRes.data
}
if (workRes.code === '0' && workRes.data) {
workList.value = workRes.data
}
if (internRes.code === '0' && internRes.data) {
internshipList.value = internRes.data
}
if (projRes.code === '0' && projRes.data) {
projectList.value = projRes.data
}
if (compRes.code === '0' && compRes.data) {
competitionList.value = compRes.data
}
}
// ==================== 生命周期 ====================
onMounted(() => {
loadResumeDetail()
// 加载诊断报告
loadDiagnosis()
})
// ==================== 计算属性 ====================
/** 头像首字母 */
function getAvatarLetter(): string {
return resumeMain.value.name?.charAt(0) || resumeMain.value.resumeName?.charAt(0) || '?'
}
// ==================== 事件处理 ====================
/** 返回简历列表页 */
function goBack() {
router.push('/resume')
}
/** 编辑简历名称弹窗显示状态 */
const editNameVisible = ref(false)
/** 编辑简历信息 — 打开编辑名称弹窗 */
function handleEdit() {
editNameVisible.value = true
}
/** 导出简历弹窗显示状态 */
const exportDialogVisible = ref(false)
/** 导出简历 — 打开导出弹窗 */
function handleExport() {
exportDialogVisible.value = true
}
/** 删除简历 — 二次确认后调用删除接口,成功后返回简历列表 */
async function handleDelete() {
try {
await ElMessageBox.confirm('确定要删除这份简历吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
// 调用删除接口
const res = await deleteResume(resumeId)
if (res.code === '0') {
ElMessage.success('删除成功')
// 返回简历列表页
router.push('/resume')
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
// 用户取消删除或接口报错
if (error !== 'cancel') {
ElMessage.error('删除失败,请稍后重试')
}
}
}
/** 查看评估报告 */
function handleViewReport() {
showReportDrawer.value = true
}
/** 简历分析报告抽屉显示状态 */
const showReportDrawer = ref(false)
/** 分析报告点击去优化 — 关闭抽屉 */
function handleClose_report() {
showReportDrawer.value = false
}
/** 重新诊断 / 开始诊断 — 调用 AI 诊断接口,完成后刷新诊断数据 */
async function handleDiagnose() {
// 全屏加载提示,AI 接口响应较慢
const loading = ElLoading.service({
lock: true,
text: '简历诊断中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
const res = await triggerResumeDiagnosis(resumeId)
if (res.code === 0) {
// 诊断完成,重新拉取诊断报告数据
await loadDiagnosis()
ElMessage.success('诊断完成')
} else {
ElMessage.error(res.msg || '诊断失败,请稍后重试')
}
} catch {
ElMessage.error('诊断请求失败,请稍后重试')
} finally {
loading.close()
}
}
/** 修复指定模块的问题 */
function handleFixIssue(moduleType: string, recordId?: string) {
fixModuleType.value = moduleType
// 确定记录 ID:个人概述用 resumeId,经历用传入的 recordId
const rid = moduleType === 'summary' ? resumeId : (recordId || '')
fixRecordId.value = rid
// 获取原文内容
fixOriginalContent.value = getOriginalContent(moduleType, rid)
// 获取匹配的诊断问题
fixIssue.value = getIssueByRecord(moduleType, rid)
showFixDrawer.value = true
}
// ==================== 问题修复抽屉相关 ====================
/** 修复抽屉显示状态 */
const showFixDrawer = ref(false)
/** 修复抽屉当前模块类型 */
const fixModuleType = ref('')
/** 修复抽屉当前记录 ID */
const fixRecordId = ref('')
/** 修复抽屉原文内容 */
const fixOriginalContent = ref<string | DescriptionParagraph[]>('')
/** 修复抽屉匹配的诊断问题 */
const fixIssue = ref<DiagnosisIssue | undefined>(undefined)
/** 根据模块类型和记录 ID 获取原文内容 */
function getOriginalContent(moduleType: string, recordId: string): string | DescriptionParagraph[] {
if (moduleType === 'summary') {
return resumeMain.value.summary || ''
}
// 5 大经历模块 — 找到对应记录的 description 数组
const listMap: Record<string, any[]> = {
education: educationList.value,
work: workList.value,
internship: internshipList.value,
project: projectList.value,
competition: competitionList.value,
}
const list = listMap[moduleType]
if (!list) return ''
const record = list.find((item: any) => item.id === recordId)
return record?.description || []
}
/** 修复抽屉提交新版本 — 更新本地数据、调用保存接口、刷新页面 */
async function handleFixSubmit(content: string[]) {
const loading = ElLoading.service({
lock: true,
text: '正在保存修改…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
const moduleType = fixModuleType.value
const recordId = fixRecordId.value
if (moduleType === 'summary') {
// 个人概述:content 只有一个元素,直接更新 summary 字段
const newSummary = content.join('\n')
resumeMain.value.summary = newSummary
await saveResumeMain({ resumeId, summary: newSummary })
} else {
// 5 大经历模块:找到对应经历记录,按 id 对应更新 description 数组里的 text
const listMap: Record<string, any[]> = {
education: educationList.value,
work: workList.value,
internship: internshipList.value,
project: projectList.value,
competition: competitionList.value,
}
const list = listMap[moduleType]
if (list) {
const record = list.find((item: any) => item.id === recordId)
if (record?.description) {
// 按顺序更新 description 数组里每个对象的 text,保留原有 id
content.forEach((text, idx) => {
if (idx < record.description.length) {
record.description[idx].text = text
}
})
// 调用对应模块的单条编辑接口(仅更新当前这条经历)
await updateSingleRecord(moduleType, record)
}
}
}
// 重新加载所有简历数据和诊断报告
await Promise.all([loadResumeDetail(), loadDiagnosis()])
ElMessage.success('修改已保存')
} catch {
ElMessage.error('保存失败,请稍后重试')
} finally {
loading.close()
}
}
/** 根据模块类型调用对应的单条编辑接口(问题修复场景,只更新一条经历的内容) */
async function updateSingleRecord(moduleType: string, record: any) {
if (moduleType === 'education') {
await updateResumeEducation({
id: record.id,
school: record.school,
major: record.major,
degree: record.degree,
studyType: record.studyType,
startDate: record.startDate,
endDate: record.endDate,
description: record.description,
})
} else if (moduleType === 'work') {
await updateResumeWork({
id: record.id,
companyName: record.companyName,
position: record.position,
startDate: record.startDate,
endDate: record.endDate,
description: record.description,
})
} else if (moduleType === 'internship') {
await updateResumeInternship({
id: record.id,
companyName: record.companyName,
position: record.position,
startDate: record.startDate,
endDate: record.endDate,
description: record.description,
})
} else if (moduleType === 'project') {
await updateResumeProject({
id: record.id,
projectName: record.projectName,
companyName: record.companyName,
role: record.role,
startDate: record.startDate,
endDate: record.endDate,
description: record.description,
})
} else if (moduleType === 'competition') {
await updateResumeCompetition({
id: record.id,
competitionName: record.competitionName,
award: record.award,
awardDate: record.awardDate,
description: record.description,
})
}
}
// ==================== 编辑抽屉相关 ====================
/** 编辑抽屉的显示状态 */
const showEditDrawer = ref(false)
/** 当前编辑的模块名称 */
const editModule = ref('info')
/** 当前编辑模块的初始数据 */
const editInitialData = ref<Record<string, any>>({})
/** 打开编辑抽屉时记录的教育经历ID快照,用于保存时做增删改diff */
const editSnapshotEducationIds = ref<string[]>([])
/** 打开编辑抽屉时记录的工作经历ID快照 */
const editSnapshotWorkIds = ref<string[]>([])
/** 打开编辑抽屉时记录的实习经历ID快照 */
const editSnapshotInternshipIds = ref<string[]>([])
/** 打开编辑抽屉时记录的项目经历ID快照 */
const editSnapshotProjectIds = ref<string[]>([])
/** 打开编辑抽屉时记录的竞赛经历ID快照 */
const editSnapshotCompetitionIds = ref<string[]>([])
/** 打开编辑抽屉 — 根据模块名设置初始数据 */
function openEditDrawer(section: string) {
editModule.value = section
if (section === 'info') {
editInitialData.value = {
name: resumeMain.value.name || '',
email: resumeMain.value.email || '',
phone: resumeMain.value.mobileNumber || '',
location: '',
wechat: resumeMain.value.wechatNumber || '',
}
} else if (section === 'summary') {
editInitialData.value = {
summary: resumeMain.value.summary || '',
}
} else if (section === 'education') {
// 记录当前教育经历的ID快照,用于保存时做增删改diff
editSnapshotEducationIds.value = educationList.value.map(edu => edu.id || '').filter(Boolean)
editInitialData.value = {
education: educationList.value.map(edu => ({
id: edu.id,
school: edu.school || '',
major: edu.major || '',
studyType: edu.studyType || '全日制',
degree: edu.degree || '本科',
startDate: edu.startDate || '',
endDate: edu.endDate || '',
description: (edu.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'work') {
// 记录当前工作经历的ID快照
editSnapshotWorkIds.value = workList.value.map(w => w.id || '').filter(Boolean)
editInitialData.value = {
works: workList.value.map(w => ({
id: w.id,
companyName: w.companyName || '',
position: w.position || '',
startDate: w.startDate || '',
endDate: w.endDate || '',
description: (w.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'internship') {
// 记录当前实习经历的ID快照
editSnapshotInternshipIds.value = internshipList.value.map(i => i.id || '').filter(Boolean)
editInitialData.value = {
internships: internshipList.value.map(i => ({
id: i.id,
companyName: i.companyName || '',
position: i.position || '',
startDate: i.startDate || '',
endDate: i.endDate || '',
description: (i.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'project') {
// 记录当前项目经历的ID快照
editSnapshotProjectIds.value = projectList.value.map(p => p.id || '').filter(Boolean)
editInitialData.value = {
projects: projectList.value.map(p => ({
id: p.id,
projectName: p.projectName || '',
companyName: p.companyName || '',
role: p.role || '',
startDate: p.startDate || '',
endDate: p.endDate || '',
description: (p.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'competition') {
// 记录当前竞赛经历的ID快照
editSnapshotCompetitionIds.value = competitionList.value.map(c => c.id || '').filter(Boolean)
editInitialData.value = {
competitions: competitionList.value.map(c => ({
id: c.id,
competitionName: c.competitionName || '',
award: c.award || '',
awardDate: c.awardDate || '',
description: (c.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'portfolio') {
editInitialData.value = {
portfolioUrl: resumeMain.value.portfolioUrl || '',
}
} else if (section === 'skills') {
editInitialData.value = {
skills: [...(resumeMain.value.skills || [])],
}
} else if (section === 'certificate') {
editInitialData.value = {
certificates: [...(resumeMain.value.certificates || [])],
}
}
showEditDrawer.value = true
}
/** 保存编辑数据 — 将抽屉返回的数据调用对应接口持久化,并更新本地数据 */
async function handleSaveEdit(data: Record<string, any>) {
try {
if (editModule.value === 'info') {
// 保存基本信息到简历主表
await saveResumeMain({
resumeId,
name: data.name,
email: data.email,
mobileNumber: data.phone,
city: data.locationName || data.location || '',
wechatNumber: data.wechat,
})
resumeMain.value.name = data.name
resumeMain.value.email = data.email
resumeMain.value.mobileNumber = data.phone
resumeMain.value.city = data.locationName || data.location || ''
resumeMain.value.wechatNumber = data.wechat
} else if (editModule.value === 'summary') {
// 保存个人概述到简历主表
await saveResumeMain({
resumeId,
summary: data.summary,
})
resumeMain.value.summary = data.summary
} else if (editModule.value === 'education') {
// 教育经历 — 通过diff对比打开编辑时的ID快照,分别调用增/改/删接口
const oldIds = new Set(editSnapshotEducationIds.value)
const newItems: any[] = data.education
const newIds = new Set(newItems.filter((edu: any) => edu.id).map((edu: any) => edu.id))
// 1. 找出被删除的经历(旧ID中有,新数据中没有的)
const deletedIds = editSnapshotEducationIds.value.filter(id => !newIds.has(id))
// 2. 遍历新数据,有id且在旧快照中存在的是编辑,没有id的是新增
const addPromises: Promise<any>[] = []
const updatePromises: Promise<any>[] = []
const deletePromises = deletedIds.map(id => deleteResumeEducation(id))
for (const edu of newItems) {
const itemData = {
school: edu.school,
major: edu.major,
degree: edu.degree,
studyType: edu.studyType,
startDate: edu.startDate,
endDate: edu.endDate,
description: edu.description,
}
if (edu.id && oldIds.has(edu.id)) {
// 已有经历 — 调用编辑接口
updatePromises.push(updateResumeEducation({ ...itemData, id: edu.id }))
} else {
// 新增经历 — 调用添加接口
addPromises.push(addResumeEducation(resumeId, itemData))
}
}
// 并发执行所有增删改请求
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
} else if (editModule.value === 'work') {
// 工作经历 — diff增删改
const oldIds = new Set(editSnapshotWorkIds.value)
const newItems: any[] = data.works
const newIds = new Set(newItems.filter((w: any) => w.id).map((w: any) => w.id))
const deletedIds = editSnapshotWorkIds.value.filter(id => !newIds.has(id))
const addPromises: Promise<any>[] = []
const updatePromises: Promise<any>[] = []
const deletePromises = deletedIds.map(id => deleteResumeWork(id))
for (const w of newItems) {
const itemData = {
companyName: w.companyName,
position: w.position,
startDate: w.startDate,
endDate: w.endDate,
description: w.description,
}
if (w.id && oldIds.has(w.id)) {
updatePromises.push(updateResumeWork({ ...itemData, id: w.id }))
} else {
addPromises.push(addResumeWork(resumeId, itemData))
}
}
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
} else if (editModule.value === 'internship') {
// 实习经历 — diff增删改
const oldIds = new Set(editSnapshotInternshipIds.value)
const newItems: any[] = data.internships
const newIds = new Set(newItems.filter((i: any) => i.id).map((i: any) => i.id))
const deletedIds = editSnapshotInternshipIds.value.filter(id => !newIds.has(id))
const addPromises: Promise<any>[] = []
const updatePromises: Promise<any>[] = []
const deletePromises = deletedIds.map(id => deleteResumeInternship(id))
for (const i of newItems) {
const itemData = {
companyName: i.companyName,
position: i.position,
startDate: i.startDate,
endDate: i.endDate,
description: i.description,
}
if (i.id && oldIds.has(i.id)) {
updatePromises.push(updateResumeInternship({ ...itemData, id: i.id }))
} else {
addPromises.push(addResumeInternship(resumeId, itemData))
}
}
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
} else if (editModule.value === 'project') {
// 项目经历 — diff增删改
const oldIds = new Set(editSnapshotProjectIds.value)
const newItems: any[] = data.projects
const newIds = new Set(newItems.filter((p: any) => p.id).map((p: any) => p.id))
const deletedIds = editSnapshotProjectIds.value.filter(id => !newIds.has(id))
const addPromises: Promise<any>[] = []
const updatePromises: Promise<any>[] = []
const deletePromises = deletedIds.map(id => deleteResumeProject(id))
for (const p of newItems) {
const itemData = {
projectName: p.projectName,
companyName: p.companyName,
role: p.role,
startDate: p.startDate,
endDate: p.endDate,
description: p.description,
}
if (p.id && oldIds.has(p.id)) {
updatePromises.push(updateResumeProject({ ...itemData, id: p.id }))
} else {
addPromises.push(addResumeProject(resumeId, itemData))
}
}
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
} else if (editModule.value === 'competition') {
// 竞赛经历 — diff增删改
const oldIds = new Set(editSnapshotCompetitionIds.value)
const newItems: any[] = data.competitions
const newIds = new Set(newItems.filter((c: any) => c.id).map((c: any) => c.id))
const deletedIds = editSnapshotCompetitionIds.value.filter(id => !newIds.has(id))
const addPromises: Promise<any>[] = []
const updatePromises: Promise<any>[] = []
const deletePromises = deletedIds.map(id => deleteResumeCompetition(id))
for (const c of newItems) {
const itemData = {
competitionName: c.competitionName,
award: c.award,
awardDate: c.awardDate,
description: c.description,
}
if (c.id && oldIds.has(c.id)) {
updatePromises.push(updateResumeCompetition({ ...itemData, id: c.id }))
} else {
addPromises.push(addResumeCompetition(resumeId, itemData))
}
}
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
} else if (editModule.value === 'portfolio') {
// 保存作品集链接到简历主表
await saveResumeMain({
resumeId,
portfolioUrl: data.portfolioUrl,
})
resumeMain.value.portfolioUrl = data.portfolioUrl
} else if (editModule.value === 'skills') {
// 保存技能到简历主表
await saveResumeMain({
resumeId,
skills: [...data.skills],
})
resumeMain.value.skills = [...data.skills]
} else if (editModule.value === 'certificate') {
// 保存证书到简历主表
await saveResumeMain({
resumeId,
certificates: [...data.certificates],
})
resumeMain.value.certificates = [...data.certificates]
}
// 保存成功后重新加载简历数据,确保 ID 等字段与服务端一致
await loadResumeDetail()
} catch {
console.error('[ResumeDetail] 保存失败')
}
}
</script>