1241 lines
52 KiB
Vue
1241 lines
52 KiB
Vue
<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>
|