1944 lines
81 KiB
Vue
1944 lines
81 KiB
Vue
<template>
|
||
<div class="resume-detail dflex">
|
||
<SideNav />
|
||
<div class="resume-detail__content">
|
||
<!-- 个人资料编辑组件(非内联模块时使用全局实例) -->
|
||
<ProfileEditDrawer
|
||
v-if="editModule !== 'info' && editModule !== 'summary' && editModule !== 'skills' && editModule !== 'certificate'"
|
||
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"
|
||
/>
|
||
|
||
<!-- 一键修复抽屉(全模块) -->
|
||
<ResumeIssueFixAllDrawer
|
||
v-model="showFixAllDrawer"
|
||
:issues="diagnosisIssues"
|
||
:original-records="allOriginalRecords"
|
||
@save="handleFixAllSave"
|
||
@use-all-ai="handleFixAllUseAi"
|
||
/>
|
||
|
||
<!-- 导出简历弹窗组件 -->
|
||
<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"
|
||
/>
|
||
|
||
<!-- 顶部标题 -->
|
||
<div class="resume-detail__page-title"><h2>我的简历</h2></div>
|
||
|
||
<!-- 顶部操作栏 -->
|
||
<div class="resume-detail__toolbar">
|
||
<div class="resume-detail__toolbar-left">
|
||
<button class="resume-detail__back-btn" @click="goBack" aria-label="返回">
|
||
<el-icon class="fs14 color-2"><ArrowLeft /></el-icon>返回
|
||
</button>
|
||
</div>
|
||
<div class="resume-detail__toolbar-right">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- 简历主体内容 -->
|
||
<div class="resume-detail__body">
|
||
|
||
<!-- 简历评分区域(不滚动) -->
|
||
<div class="resume-detail__fixed-top">
|
||
|
||
<!-- 简历评分区域 -->
|
||
<div class="resume-detail__score-bar border-F0F0F0-bottom-1 border-ra0" :class="scoreBarClass">
|
||
<div class="resume-detail__score-left">
|
||
<!-- 星星+底座组合 -->
|
||
<div class="resume-detail__star-wrapper">
|
||
|
||
|
||
<div class="w40 tac h40 lh38 border-ra50p " :style="{ background: starStrokeColor }">
|
||
<span class="color-f fs18 fw600 dib">{{ hasDiagnosis ? diagnosisReport.grade : '?' }}</span>
|
||
</div>
|
||
|
||
</div>
|
||
<!-- 右侧操作按钮区 -->
|
||
<div class="resume-detail__score-actions">
|
||
<template v-if="hasDiagnosis">
|
||
<div class="hp100">
|
||
<!-- 底座长方形(遮住星星下方两角) -->
|
||
<div class="resume-detail__star-base ">
|
||
<span :style="{ color: starStrokeColor }">{{ hasDiagnosis ? gradeLabel : '未知' }}</span>
|
||
</div>
|
||
<div class="mt0 resume-detail__score-links">
|
||
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告</button>
|
||
<button class="resume-detail__score-link" @click="handleDiagnose">重新诊断</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div>
|
||
<span class="resume-detail__score-hint">暂无等级,点击开始诊断简历</span>
|
||
<div class="fs12 color-light mt4">AI 诊断将深度分析您的简历,给出评分和优化建议</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="resume-detail__score-right">
|
||
<!-- 有诊断报告时显示计数和重新诊断按钮 -->
|
||
<template v-if="hasDiagnosis">
|
||
<div class="resume-detail__score-item">
|
||
<span class="resume-detail__score-num" :style="{ color: starStrokeColor }">{{ issuesUrgentTotal+issuesImportantTotal+issuesExpressionTotal }}</span>
|
||
<span class="resume-detail__score-label" :style="{ color: starStrokeColor }">项待修复</span>
|
||
<button class="resume-detail__diagnose-btn" :style="{ background: starStrokeColor }" @click="handleFixAll"><svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<g clip-path="url(#clip0_76_1547)">
|
||
<path d="M5.93736 4.4335L3.9582 5.54183L5.06653 3.56266L3.9582 1.5835L5.93736 2.69183L7.91653 1.5835L6.8082 3.56266L7.91653 5.54183L5.93736 4.4335ZM15.4374 12.1918L17.4165 11.0835L16.3082 13.0627L17.4165 15.0418L15.4374 13.9335L13.4582 15.0418L14.5665 13.0627L13.4582 11.0835L15.4374 12.1918ZM17.4165 1.5835L16.3082 3.56266L17.4165 5.54183L15.4374 4.4335L13.4582 5.54183L14.5665 3.56266L13.4582 1.5835L15.4374 2.69183L17.4165 1.5835ZM10.5607 10.1177L12.4924 8.186L10.814 6.50766L8.88236 8.43933L10.5607 10.1177ZM11.3761 5.77141L13.2286 7.62391C13.5374 7.91683 13.5374 8.43141 13.2286 8.74016L3.98986 17.9789C3.68111 18.2877 3.16653 18.2877 2.87361 17.9789L1.02111 16.1264C0.712363 15.8335 0.712363 15.3189 1.02111 15.0102L10.2599 5.77141C10.5686 5.46266 11.0832 5.46266 11.3761 5.77141Z" fill="white"/>
|
||
</g>
|
||
<defs>
|
||
<clipPath id="clip0_76_1547">
|
||
<rect width="19" height="19" fill="white"/>
|
||
</clipPath>
|
||
</defs>
|
||
</svg>
|
||
<span class="verali-su fw600 ml5">一键修复</span></button>
|
||
</div>
|
||
</template>
|
||
<!-- 没有诊断报告时显示开始诊断按钮 -->
|
||
<template v-else>
|
||
<button class="resume-detail__diagnose-btn dflex-center aliite-c" @click="handleDiagnose">
|
||
<svg width="20" height="20" viewBox="0 0 19 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<g clip-path="url(#clip0_76_1376)">
|
||
<path d="M16.753 15.5746L13.4589 12.2793C14.4466 10.9923 14.9077 9.37781 14.7488 7.76335C14.5898 6.14889 13.8226 4.65534 12.6029 3.58569C11.3832 2.51603 9.80222 1.95036 8.18074 2.00342C6.55927 2.05649 5.01869 2.72431 3.87152 3.87143C2.72435 5.01855 2.05649 6.55906 2.00342 8.18046C1.95036 9.80186 2.51605 11.3827 3.58576 12.6024C4.65547 13.8221 6.14908 14.5892 7.76362 14.7482C9.37816 14.9071 10.9927 14.446 12.2798 13.4584L15.5766 16.7558C15.654 16.8332 15.746 16.8946 15.8471 16.9365C15.9483 16.9784 16.0567 17 16.1662 17C16.2757 17 16.3841 16.9784 16.4853 16.9365C16.5864 16.8946 16.6784 16.8332 16.7558 16.7558C16.8332 16.6784 16.8946 16.5865 16.9365 16.4853C16.9784 16.3841 17 16.2757 17 16.1662C17 16.0567 16.9784 15.9483 16.9365 15.8472C16.8946 15.746 16.8332 15.6541 16.7558 15.5767L16.753 15.5746ZM3.67871 8.39513C3.67871 7.46229 3.95534 6.55041 4.47362 5.77478C4.9919 4.99916 5.72855 4.39464 6.59042 4.03766C7.45228 3.68068 8.40065 3.58728 9.31561 3.76926C10.2306 3.95125 11.071 4.40045 11.7306 5.06006C12.3903 5.71968 12.8395 6.56008 13.0215 7.47498C13.2035 8.38989 13.1101 9.33822 12.7531 10.2C12.3961 11.0619 11.7915 11.7985 11.0159 12.3167C10.2402 12.835 9.3283 13.1116 8.39542 13.1116C7.14487 13.1103 5.9459 12.613 5.06163 11.7288C4.17735 10.8445 3.68 9.64562 3.67871 8.39513Z" fill="white"/>
|
||
</g>
|
||
<defs>
|
||
<clipPath id="clip0_76_1376">
|
||
<rect width="19" height="19" fill="white"/>
|
||
</clipPath>
|
||
</defs>
|
||
</svg>
|
||
<span class=" fs14 fw600 ml3">开始诊断</span></button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab 导航栏(固定不动) -->
|
||
<div class="resume-detail__tabs">
|
||
<span
|
||
v-for="tab in resumeTabList"
|
||
:key="tab.key"
|
||
class="resume-detail__tab-item"
|
||
:class="{ 'resume-detail__tab-item--active': resumeActiveTab === tab.key }"
|
||
@click="scrollToResumeSection(tab.key)"
|
||
>{{ tab.label }}</span>
|
||
</div>
|
||
|
||
</div><!-- 固定顶部区域结束 -->
|
||
|
||
<!-- 可滚动内容区域 -->
|
||
<div ref="resumeScrollRef" class="resume-detail__scroll-body">
|
||
|
||
<!-- 个人信息卡片 -->
|
||
<div ref="resumeSectionInfoRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'info' || !showEditDrawer }">
|
||
<!-- 展示模式 -->
|
||
<template v-if="editModule !== 'info' || !showEditDrawer">
|
||
<div class="resume-detail__card-header color-6">
|
||
<div class="dflex-start aliite-c">
|
||
<h3 class="resume-detail__user-name">{{ resumeMain.name || '未填写姓名' }}</h3>
|
||
<span v-if="resumeMain.city" class="ml10 dflex-start aliite-c fs13">
|
||
<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>
|
||
</div>
|
||
<!-- 编辑按钮 — hover时才显示,品牌色图标+文字 -->
|
||
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('info')" aria-label="编辑个人信息">
|
||
<el-icon><EditPen /></el-icon>
|
||
<span>编辑</span>
|
||
</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>
|
||
<span>{{ resumeMain.email }}</span>
|
||
</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>
|
||
<span>{{ resumeMain.mobileNumber }}</span>
|
||
</span>
|
||
|
||
<!-- 微信号 -->
|
||
<span v-if="resumeMain.wechatNumber" class="resume-detail__contact-item">
|
||
<el-icon class="fs14 color-9"><ChatLineRound /></el-icon>
|
||
<span>{{ resumeMain.wechatNumber }}</span>
|
||
</span>
|
||
<!-- 作品集链接 -->
|
||
<span v-if="resumeMain.portfolioUrl" class="resume-detail__contact-item dib">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class=" mr6" width="0.15rem" height="0.15rem" viewBox="0 0 16 16">
|
||
<path d="M0 0h16v16H0z" fill="none" />
|
||
<path fill="none" stroke="currentColor" d="M14.5 8A6.5 6.5 0 0 1 8 14.5M14.5 8A6.5 6.5 0 0 0 8 1.5M14.5 8c0 1.657-2.91 3-6.5 3S1.5 9.657 1.5 8m13 0c0-1.657-2.91-3-6.5-3S1.5 6.343 1.5 8M8 14.5A6.5 6.5 0 0 1 1.5 8M8 14.5c1.657 0 3-2.91 3-6.5S9.657 1.5 8 1.5m0 13c-1.657 0-3-2.91-3-6.5s1.343-6.5 3-6.5M1.5 8A6.5 6.5 0 0 1 8 1.5" />
|
||
</svg>
|
||
|
||
|
||
<a :href="resumeMain.portfolioUrl?.startsWith('http') ? resumeMain.portfolioUrl : `http://${resumeMain.portfolioUrl}`" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">{{ resumeMain.portfolioUrl }}</a>
|
||
</span>
|
||
</div>
|
||
</template>
|
||
<!-- 编辑模式 — 内联编辑表单 -->
|
||
<ProfileEditDrawer
|
||
v-if="editModule === 'info' && showEditDrawer"
|
||
v-model="showEditDrawer"
|
||
:module="editModule"
|
||
:initial-data="editInitialData"
|
||
:use-education-text-value="true"
|
||
@save="handleSaveEdit"
|
||
@cancel="showEditDrawer = false"
|
||
/>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 个人概述 -->
|
||
<div ref="resumeSectionSummaryRef" class="resume-detail__card mt14" :class="{ 'resume-detail__card-hover': editModule !== 'summary' || !showEditDrawer }">
|
||
<!-- 展示模式 -->
|
||
<template v-if="editModule !== 'summary' || !showEditDrawer">
|
||
<div class="resume-detail__section-header pl0">
|
||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">个人概述</h3>
|
||
<!-- 有修复建议时显示标记(hover 显示诊断详情气泡) -->
|
||
<span v-if="hasDiagnosis && hasModuleIssue('summary')" class="resume-detail__fix-tag">
|
||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||
建议使用AI润色修复 >
|
||
<!-- hover 气泡 -->
|
||
<span class="resume-detail__fix-tooltip">{{ getSummaryFinding() }}</span>
|
||
</span>
|
||
<!-- 编辑按钮 — hover时显示 -->
|
||
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('summary')" aria-label="编辑个人概述">
|
||
<el-icon><EditPen /></el-icon>
|
||
<span>编辑</span>
|
||
</button>
|
||
</div>
|
||
<p class="resume-detail__summary-text">{{ resumeMain.summary }}</p>
|
||
</template>
|
||
<!-- 编辑模式 — 内联编辑表单 -->
|
||
<ProfileEditDrawer
|
||
v-if="editModule === 'summary' && showEditDrawer"
|
||
v-model="showEditDrawer"
|
||
:module="editModule"
|
||
:initial-data="editInitialData"
|
||
:use-education-text-value="true"
|
||
@save="handleSaveEdit"
|
||
@cancel="showEditDrawer = false"
|
||
/>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 教育背景经历 -->
|
||
<div ref="resumeSectionEducationRef" class="resume-detail__card">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">教育背景</h3>
|
||
<!-- 添加按钮 — hover模块时显示 -->
|
||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('education')" aria-label="添加教育经历">
|
||
<el-icon><Plus /></el-icon>
|
||
<span>添加</span>
|
||
</button>
|
||
</div>
|
||
<!-- 新增经历编辑区(显示在第一段经历前面) -->
|
||
<ResumeExpInlineEdit
|
||
v-if="expAddingModule === 'education'"
|
||
:module-type="'education'"
|
||
@save="handleExpAdd('education', $event)"
|
||
@cancel="expAddingModule = ''"
|
||
/>
|
||
<div v-for="edu in educationList" :key="edu.id" class="resume-detail__exp-item" @click.stop="startEditExp('education', edu)">
|
||
<!-- 展示模式 -->
|
||
<template v-if="expEditingId !== edu.id">
|
||
<div class="resume-detail__exp-row">
|
||
<span class="resume-detail__exp-name">{{ edu.school }}</span>
|
||
<span v-if="edu.major || edu.degree" class="resume-detail__exp-role">{{ edu.major }}{{ edu.degree ? '·' + edu.degree : '' }}</span>
|
||
<span v-if="hasDiagnosis && getIssueByRecord('education', edu.id!) && getIssueByRecord('education', edu.id!)!.status === 0" class="resume-detail__fix-tag">
|
||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||
建议使用AI润色修复
|
||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('education', edu.id!)!.finding }}</span>
|
||
</span>
|
||
<div class="mlauto dflex-end">
|
||
<!-- 时间(默认显示,hover时隐藏) -->
|
||
<span v-if="edu.startDate || edu.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ edu.startDate }}<span v-if="edu.startDate && edu.endDate">-</span>{{ edu.endDate }}</span>
|
||
<!-- 编辑按钮 — hover时显示 -->
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('education', edu)" aria-label="编辑">
|
||
<el-icon><EditPen /></el-icon>
|
||
<span>编辑</span>
|
||
</button>
|
||
<!-- 删除按钮 -->
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click.stop="handleExpDelete('education', edu.id!)" aria-label="删除">
|
||
<el-icon><DeleteFilled /></el-icon>
|
||
<span>删除</span>
|
||
</button>
|
||
</div>
|
||
</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>
|
||
</template>
|
||
<!-- 编辑模式 -->
|
||
<ResumeExpInlineEdit
|
||
v-else
|
||
:module-type="'education'"
|
||
:initial-data="edu"
|
||
@save="handleExpUpdate('education', edu.id!, $event)"
|
||
@cancel="expEditingId = ''"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 工作经历 -->
|
||
<div ref="resumeSectionWorkRef" class="resume-detail__card">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">工作经历</h3>
|
||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('work')" aria-label="添加工作经历">
|
||
<el-icon><Plus /></el-icon>
|
||
<span>添加</span>
|
||
</button>
|
||
</div>
|
||
<ResumeExpInlineEdit v-if="expAddingModule === 'work'" :module-type="'work'" @save="handleExpAdd('work', $event)" @cancel="expAddingModule = ''" />
|
||
<div v-for="exp in workList" :key="exp.id" class="resume-detail__exp-item" @click.stop="startEditExp('work', exp)">
|
||
<template v-if="expEditingId !== exp.id">
|
||
<div class="resume-detail__exp-row">
|
||
<span class="resume-detail__exp-name">{{ exp.companyName }}</span>
|
||
<span v-if="exp.position" class="resume-detail__exp-role">{{ exp.position }}</span>
|
||
<span v-if="hasDiagnosis && getIssueByRecord('work', exp.id!) && getIssueByRecord('work', exp.id!)!.status === 0" class="resume-detail__fix-tag">
|
||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||
建议使用AI润色修复
|
||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('work', exp.id!)!.finding }}</span>
|
||
</span>
|
||
<div class="mlauto dflex-end">
|
||
<!-- 时间(默认显示,hover时隐藏) -->
|
||
<span v-if="exp.startDate || exp.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ exp.startDate }}<span v-if="exp.startDate && exp.endDate">-</span>{{ exp.endDate }}</span>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('work', exp)" aria-label="编辑">
|
||
<el-icon><EditPen /></el-icon><span>编辑</span>
|
||
</button>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click.stop="handleExpDelete('work', exp.id!)" aria-label="删除">
|
||
<el-icon><DeleteFilled /></el-icon><span>删除</span>
|
||
</button>
|
||
</div>
|
||
</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">{{ p.text }}</li>
|
||
</ul>
|
||
</template>
|
||
<ResumeExpInlineEdit v-else :module-type="'work'" :initial-data="exp" @save="handleExpUpdate('work', exp.id!, $event)" @cancel="expEditingId = ''" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 实习经历 -->
|
||
<div ref="resumeSectionInternshipRef" class="resume-detail__card">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">实习经历</h3>
|
||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('internship')" aria-label="添加实习经历">
|
||
<el-icon><Plus /></el-icon><span>添加</span>
|
||
</button>
|
||
</div>
|
||
<ResumeExpInlineEdit v-if="expAddingModule === 'internship'" :module-type="'internship'" @save="handleExpAdd('internship', $event)" @cancel="expAddingModule = ''" />
|
||
<div v-for="intern in internshipList" :key="intern.id" class="resume-detail__exp-item" @click.stop="startEditExp('internship', intern)">
|
||
<template v-if="expEditingId !== intern.id">
|
||
<div class="resume-detail__exp-row">
|
||
<span class="resume-detail__exp-name">{{ intern.companyName }}</span>
|
||
<span v-if="intern.position" class="resume-detail__exp-role">{{ intern.position }}</span>
|
||
<span v-if="hasDiagnosis && getIssueByRecord('internship', intern.id!) && getIssueByRecord('internship', intern.id!)!.status === 0" class="resume-detail__fix-tag">
|
||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||
建议使用AI润色修复
|
||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('internship', intern.id!)!.finding }}</span>
|
||
</span>
|
||
<div class="mlauto dflex-end">
|
||
<!-- 时间(默认显示,hover时隐藏) -->
|
||
<span v-if="intern.startDate || intern.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ intern.startDate }}<span v-if="intern.startDate && intern.endDate">-</span>{{ intern.endDate }}</span>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('internship', intern)" aria-label="编辑">
|
||
<el-icon><EditPen /></el-icon><span>编辑</span>
|
||
</button>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click.stop="handleExpDelete('internship', intern.id!)" aria-label="删除">
|
||
<el-icon><DeleteFilled /></el-icon><span>删除</span>
|
||
</button>
|
||
</div>
|
||
</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>
|
||
</template>
|
||
<ResumeExpInlineEdit v-else :module-type="'internship'" :initial-data="intern" @save="handleExpUpdate('internship', intern.id!, $event)" @cancel="expEditingId = ''" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 项目经历 -->
|
||
<div ref="resumeSectionProjectRef" class="resume-detail__card">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">项目经历</h3>
|
||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('project')" aria-label="添加项目经历">
|
||
<el-icon><Plus /></el-icon><span>添加</span>
|
||
</button>
|
||
</div>
|
||
<ResumeExpInlineEdit v-if="expAddingModule === 'project'" :module-type="'project'" @save="handleExpAdd('project', $event)" @cancel="expAddingModule = ''" />
|
||
<div v-for="proj in projectList" :key="proj.id" class="resume-detail__exp-item" @click.stop="startEditExp('project', proj)">
|
||
<template v-if="expEditingId !== proj.id">
|
||
<div class="resume-detail__exp-row">
|
||
<span class="resume-detail__exp-name">{{ proj.projectName }}</span>
|
||
<span v-if="proj.role" class="resume-detail__exp-role">{{ proj.role }}</span>
|
||
<span v-if="hasDiagnosis && getIssueByRecord('project', proj.id!) && getIssueByRecord('project', proj.id!)!.status === 0" class="resume-detail__fix-tag">
|
||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||
建议使用AI润色修复
|
||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('project', proj.id!)!.finding }}</span>
|
||
</span>
|
||
<div class="mlauto dflex-end">
|
||
<!-- 时间(默认显示,hover时隐藏) -->
|
||
<span v-if="proj.startDate || proj.endDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ proj.startDate }}<span v-if="proj.startDate && proj.endDate">-</span>{{ proj.endDate }}</span>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('project', proj)" aria-label="编辑">
|
||
<el-icon><EditPen /></el-icon><span>编辑</span>
|
||
</button>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click.stop="handleExpDelete('project', proj.id!)" aria-label="删除">
|
||
<el-icon><DeleteFilled /></el-icon><span>删除</span>
|
||
</button>
|
||
</div>
|
||
</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>
|
||
</template>
|
||
<ResumeExpInlineEdit v-else :module-type="'project'" :initial-data="proj" @save="handleExpUpdate('project', proj.id!, $event)" @cancel="expEditingId = ''" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 竞赛经历 -->
|
||
<div ref="resumeSectionCompetitionRef" class="resume-detail__card">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">竞赛经历</h3>
|
||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('competition')" aria-label="添加竞赛经历">
|
||
<el-icon><Plus /></el-icon><span>添加</span>
|
||
</button>
|
||
</div>
|
||
<ResumeExpInlineEdit v-if="expAddingModule === 'competition'" :module-type="'competition'" @save="handleExpAdd('competition', $event)" @cancel="expAddingModule = ''" />
|
||
<div v-for="comp in competitionList" :key="comp.id" class="resume-detail__exp-item" @click.stop="startEditExp('competition', comp)">
|
||
<template v-if="expEditingId !== comp.id">
|
||
<div class="resume-detail__exp-row">
|
||
<span class="resume-detail__exp-name">{{ comp.competitionName }}</span>
|
||
<span v-if="comp.award" class="resume-detail__exp-role">{{ comp.award }}</span>
|
||
<span v-if="hasDiagnosis && getIssueByRecord('competition', comp.id!) && getIssueByRecord('competition', comp.id!)!.status === 0" class="resume-detail__fix-tag">
|
||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||
建议使用AI润色修复
|
||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('competition', comp.id!)!.finding }}</span>
|
||
</span>
|
||
<div class="mlauto dflex-end">
|
||
<!-- 时间(默认显示,hover时隐藏) -->
|
||
<span v-if="comp.awardDate" class="resume-detail__exp-date resume-detail__exp-date--toggle">{{ comp.awardDate }}</span>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('competition', comp)" aria-label="编辑">
|
||
<el-icon><EditPen /></el-icon><span>编辑</span>
|
||
</button>
|
||
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click.stop="handleExpDelete('competition', comp.id!)" aria-label="删除">
|
||
<el-icon><DeleteFilled /></el-icon><span>删除</span>
|
||
</button>
|
||
</div>
|
||
</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>
|
||
</template>
|
||
<ResumeExpInlineEdit v-else :module-type="'competition'" :initial-data="comp" @save="handleExpUpdate('competition', comp.id!, $event)" @cancel="expEditingId = ''" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 技能 -->
|
||
<div ref="resumeSectionSkillsRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'skills' || !showEditDrawer }">
|
||
<template v-if="editModule !== 'skills' || !showEditDrawer">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title">技能</h3>
|
||
<!-- 编辑按钮 — hover时显示 -->
|
||
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('skills')" aria-label="编辑技能">
|
||
<el-icon><EditPen /></el-icon>
|
||
<span>编辑</span>
|
||
</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>
|
||
</template>
|
||
<!-- 编辑模式 -->
|
||
<ProfileEditDrawer
|
||
v-if="editModule === 'skills' && showEditDrawer"
|
||
v-model="showEditDrawer"
|
||
:module="editModule"
|
||
:initial-data="editInitialData"
|
||
:use-education-text-value="true"
|
||
@save="handleSaveEdit"
|
||
@cancel="showEditDrawer = false"
|
||
/>
|
||
</div>
|
||
|
||
<div class="h1 border-EDF9FA-bottom-2 mrl32"></div>
|
||
|
||
<!-- 证书 -->
|
||
<div ref="resumeSectionCertificateRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'certificate' || !showEditDrawer }">
|
||
<template v-if="editModule !== 'certificate' || !showEditDrawer">
|
||
<div class="resume-detail__section-header">
|
||
<h3 class="resume-detail__section-title">证书</h3>
|
||
<!-- 编辑按钮 — hover时显示 -->
|
||
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('certificate')" aria-label="编辑证书">
|
||
<el-icon><EditPen /></el-icon>
|
||
<span>编辑</span>
|
||
</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>
|
||
</template>
|
||
<!-- 编辑模式 -->
|
||
<ProfileEditDrawer
|
||
v-if="editModule === 'certificate' && showEditDrawer"
|
||
v-model="showEditDrawer"
|
||
:module="editModule"
|
||
:initial-data="editInitialData"
|
||
:use-education-text-value="true"
|
||
@save="handleSaveEdit"
|
||
@cancel="showEditDrawer = false"
|
||
/>
|
||
</div>
|
||
</div><!-- 可滚动内容区域结束 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 会员权限拦截弹窗 -->
|
||
<MemberAccessDialog v-model="showMemberAccessDialog" @open-member="showMemberDialog = true" />
|
||
<!-- 会员购买弹窗 -->
|
||
<MemberDialog v-model="showMemberDialog" />
|
||
<!-- 全屏加载遮罩 -->
|
||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||
<!-- 简历诊断步骤进度遮罩 -->
|
||
<StepProgressOverlay
|
||
v-if="showDiagnoseProgress"
|
||
:title="diagnoseProgressTitle"
|
||
:subtitle="diagnoseProgressSubtitle"
|
||
:steps="diagnoseProgressSteps"
|
||
:durations="diagnoseProgressDurations"
|
||
:done="diagnoseProgressDone"
|
||
@finished="handleDiagnoseProgressFinished"
|
||
@close="handleDiagnoseProgressClose"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { useStore } from 'vuex'
|
||
import SideNav from '@/components/SideNav.vue'
|
||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||
import ResumeExpInlineEdit from '@/components/ResumeExpInlineEdit.vue'
|
||
import ResumeAnalysisReportDrawer from '@/components/ResumeAnalysisReportDrawer.vue'
|
||
import ResumeIssueFixDrawer from '@/components/ResumeIssueFixDrawer.vue'
|
||
import ResumeIssueFixAllDrawer from '@/components/ResumeIssueFixAllDrawer.vue'
|
||
import type { FixAllSubmitItem, OriginalRecordData } from '@/components/ResumeIssueFixAllDrawer.vue'
|
||
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
|
||
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
||
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
|
||
import MemberDialog from '@/components/MemberDialog.vue'
|
||
import { fetchMemberStatus } from '@/api/member'
|
||
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,
|
||
resolveDiagnosisIssue,
|
||
type ResumeMainData,
|
||
type ResumeEducation,
|
||
type ResumeWork,
|
||
type ResumeInternship,
|
||
type ResumeProject,
|
||
type ResumeCompetition,
|
||
type DiagnosisReport,
|
||
type DiagnosisIssue,
|
||
type DescriptionParagraph,
|
||
} from '@/api/resume'
|
||
import {ArrowLeft, EditPen, Plus, DeleteFilled, ChatLineRound} from "@element-plus/icons-vue";
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||
|
||
// ==================== Store 实例 ====================
|
||
const store = useStore()
|
||
|
||
// ==================== Tab 导航栏相关 ====================
|
||
|
||
/** 简历详情 Tab 导航列表 */
|
||
const resumeTabList = [
|
||
{ key: 'info', label: '个人信息' },
|
||
{ key: 'summary', label: '个人概述' },
|
||
{ key: 'education', label: '教育背景' },
|
||
{ key: 'work', label: '工作经历' },
|
||
{ key: 'internship', label: '实习经历' },
|
||
{ key: 'project', label: '项目经历' },
|
||
{ key: 'competition', label: '竞赛经历' },
|
||
{ key: 'skills', label: '技能' },
|
||
{ key: 'certificate', label: '证书' },
|
||
]
|
||
|
||
/** 当前激活的 Tab */
|
||
const resumeActiveTab = ref('info')
|
||
|
||
/** 可滚动区域的 DOM 引用 */
|
||
const resumeScrollRef = ref<HTMLElement | null>(null)
|
||
|
||
/** 各模块的 DOM 引用 */
|
||
const resumeSectionInfoRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionSummaryRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionEducationRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionWorkRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionInternshipRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionProjectRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionCompetitionRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionSkillsRef = ref<HTMLElement | null>(null)
|
||
const resumeSectionCertificateRef = ref<HTMLElement | null>(null)
|
||
|
||
/** 模块 key 与 ref 的映射 */
|
||
const resumeSectionRefMap: Record<string, typeof resumeSectionInfoRef> = {
|
||
info: resumeSectionInfoRef,
|
||
summary: resumeSectionSummaryRef,
|
||
education: resumeSectionEducationRef,
|
||
work: resumeSectionWorkRef,
|
||
internship: resumeSectionInternshipRef,
|
||
project: resumeSectionProjectRef,
|
||
competition: resumeSectionCompetitionRef,
|
||
skills: resumeSectionSkillsRef,
|
||
certificate: resumeSectionCertificateRef,
|
||
}
|
||
|
||
/** 点击 Tab 滚动到对应模块 */
|
||
function scrollToResumeSection(key: string) {
|
||
resumeActiveTab.value = key
|
||
const targetRef = resumeSectionRefMap[key]
|
||
const container = resumeScrollRef.value
|
||
if (targetRef?.value && container) {
|
||
const offsetTop = targetRef.value.offsetTop - container.offsetTop
|
||
container.scrollTo({ top: offsetTop, behavior: 'smooth' })
|
||
}
|
||
}
|
||
|
||
// ==================== 全屏加载状态 ====================
|
||
/** 全屏加载是否显示 */
|
||
const fullLoading = ref(false)
|
||
/** 全屏加载文案 */
|
||
const fullLoadingText = ref('')
|
||
|
||
// ==================== 诊断数据相关 ====================
|
||
|
||
/** 会员权限拦截弹窗 */
|
||
const showMemberAccessDialog = ref(false)
|
||
/** 会员购买弹窗 */
|
||
const showMemberDialog = ref(false)
|
||
|
||
/** 问题类型枚举值 */
|
||
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 || ''] || '未评级'
|
||
})
|
||
|
||
/** 星星边框/线条颜色 */
|
||
const starStrokeColor = computed(() => {
|
||
if (!hasDiagnosis.value) return '#858585'
|
||
if (diagnosisReport.value.grade === 'A') return '#12C7BE'
|
||
return '#FF8D0B'
|
||
})
|
||
|
||
/** 星星填充背景色 */
|
||
const starFillColor = computed(() => {
|
||
if (!hasDiagnosis.value) return '#F4F4F4'
|
||
if (diagnosisReport.value.grade === 'A') return '#ECFDFC'
|
||
return '#FFF5E9'
|
||
})
|
||
|
||
/** 评分区域外层 class(控制背景渐变) */
|
||
const scoreBarClass = computed(() => {
|
||
if (!hasDiagnosis.value) return ''
|
||
if (diagnosisReport.value.grade === 'A') return 'resume-detail__score-bar--grade-a'
|
||
return 'resume-detail__score-bar--grade-bcd'
|
||
})
|
||
|
||
/**
|
||
* 从 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)
|
||
}
|
||
|
||
/** 判断某个模块是否存在待处理的诊断问题 */
|
||
function hasModuleIssue(moduleType: string): boolean {
|
||
return diagnosisIssues.value.some(i => i.moduleType === moduleType && i.status === 0)
|
||
}
|
||
|
||
/** 获取个人概述的 finding 文字(用于 hover 气泡) */
|
||
function getSummaryFinding(): string {
|
||
const issue = diagnosisIssues.value.find(i => i.moduleType === 'summary' && i.status === 0)
|
||
return issue?.finding || ''
|
||
}
|
||
|
||
/** 根据模块类型和列表索引获取对应的 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[]>([])
|
||
|
||
// ==================== 数据加载 ====================
|
||
|
||
/** 根据城市名称从地区树中反查城市编码(用于后端未返回 regionCode 时的 fallback) */
|
||
function resolveRegionCodeByName(cityName: string): string {
|
||
if (!cityName) return ''
|
||
const regions = store.state.regions || []
|
||
for (const province of regions) {
|
||
for (const city of province.children) {
|
||
if (city.name === cityName) return city.code
|
||
if (city.children) {
|
||
for (const district of city.children) {
|
||
if (district.name === cityName) return district.code
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/** 加载所有简历详情数据 */
|
||
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
|
||
// 如果后端未返回 regionCode 但有 city 名称,尝试从地区树反查编码
|
||
if (!resumeMain.value.regionCode && resumeMain.value.city) {
|
||
resumeMain.value.regionCode = resolveRegionCodeByName(resumeMain.value.city)
|
||
}
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
// ==================== 地区数据反查 ====================
|
||
|
||
/** 当 store 中地区树数据加载完毕后,如果 resumeMain 缺少 regionCode 则尝试反查 */
|
||
watch(
|
||
() => store.state.regions,
|
||
(regions) => {
|
||
if (regions?.length && resumeMain.value.city && !resumeMain.value.regionCode) {
|
||
resumeMain.value.regionCode = resolveRegionCodeByName(resumeMain.value.city)
|
||
}
|
||
}
|
||
)
|
||
|
||
// ==================== 生命周期 ====================
|
||
|
||
onMounted(() => {
|
||
loadResumeDetail()
|
||
// 加载诊断报告
|
||
loadDiagnosis()
|
||
// 确保地区分类数据可用(页面刷新后 store 可能为空)
|
||
store.dispatch('loadCommonData')
|
||
})
|
||
|
||
// ==================== 计算属性 ====================
|
||
|
||
/** 头像首字母 */
|
||
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
|
||
}
|
||
|
||
/** 重新诊断 / 开始诊断 — 先检查会员状态,非会员弹权限拦截弹窗 */
|
||
async function handleDiagnose() {
|
||
// 先检查会员状态
|
||
try {
|
||
const statusRes = await fetchMemberStatus()
|
||
if (statusRes.code === '0' && statusRes.data && !statusRes.data.isMember) {
|
||
// 非会员 — 弹出会员权限拦截弹窗
|
||
showMemberAccessDialog.value = true
|
||
return
|
||
}
|
||
} catch {
|
||
// 接口异常时不阻断(可能是网络问题),继续执行诊断
|
||
}
|
||
|
||
// 显示步骤进度遮罩
|
||
showDiagnoseProgress.value = true
|
||
diagnoseProgressDone.value = false
|
||
try {
|
||
const res = await triggerResumeDiagnosis(resumeId)
|
||
if (res.code === 0) {
|
||
// 诊断完成,重新拉取诊断报告数据
|
||
await loadDiagnosis()
|
||
// 通知进度组件接口已完成
|
||
diagnoseProgressDone.value = true
|
||
} else {
|
||
showDiagnoseProgress.value = false
|
||
ElMessage.error(res.msg || '诊断失败,请稍后重试')
|
||
}
|
||
} catch {
|
||
showDiagnoseProgress.value = false
|
||
ElMessage.error('诊断请求失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
// ==================== 简历诊断步骤进度遮罩 ====================
|
||
|
||
/** 诊断步骤进度遮罩是否显示 */
|
||
const showDiagnoseProgress = ref(false)
|
||
/** 诊断接口是否已完成 */
|
||
const diagnoseProgressDone = ref(false)
|
||
/** 诊断大标题 */
|
||
const diagnoseProgressTitle = '简历诊断中,请耐心等待'
|
||
/** 诊断副标题 */
|
||
const diagnoseProgressSubtitle = '根据STAR法则,快速校验你的简历缺失部分并且评分'
|
||
/** 诊断4个步骤名称 */
|
||
const diagnoseProgressSteps = ['教育背景解析...', '工作经历解析...', '技能特长解析...', '评分结果最终确认...']
|
||
/** 每步时长6000毫秒 */
|
||
const diagnoseProgressDurations = [6000, 6000, 6000, 6000]
|
||
|
||
/** 诊断进度走完后提示成功 */
|
||
function handleDiagnoseProgressFinished() {
|
||
showDiagnoseProgress.value = false
|
||
ElMessage.success('诊断完成')
|
||
}
|
||
|
||
/** 关闭诊断进度遮罩 */
|
||
function handleDiagnoseProgressClose() {
|
||
showDiagnoseProgress.value = false
|
||
}
|
||
|
||
/** 修复指定模块的问题 */
|
||
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 || []
|
||
}
|
||
|
||
// ==================== 一键修复抽屉(全模块)相关 ====================
|
||
|
||
/** 一键修复抽屉显示状态 */
|
||
const showFixAllDrawer = ref(false)
|
||
|
||
/** 构建所有模块的原始经历数据(传给一键修复组件) */
|
||
const allOriginalRecords = computed<OriginalRecordData[]>(() => {
|
||
const records: OriginalRecordData[] = []
|
||
|
||
// summary
|
||
if (resumeMain.value.summary) {
|
||
records.push({
|
||
moduleType: 'summary',
|
||
recordId: resumeId,
|
||
recordLabel: '',
|
||
originalContent: resumeMain.value.summary,
|
||
})
|
||
}
|
||
|
||
// 教育经历
|
||
for (const edu of educationList.value) {
|
||
if (!edu.id) continue
|
||
records.push({
|
||
moduleType: 'education',
|
||
recordId: edu.id,
|
||
recordLabel: `${edu.school || ''}${edu.major ? '-' + edu.major : ''}-经历描述`,
|
||
originalContent: edu.description || [],
|
||
})
|
||
}
|
||
|
||
// 工作经历
|
||
for (const work of workList.value) {
|
||
if (!work.id) continue
|
||
records.push({
|
||
moduleType: 'work',
|
||
recordId: work.id,
|
||
recordLabel: `${work.companyName || ''}${work.position ? '-' + work.position : ''}-经历描述`,
|
||
originalContent: work.description || [],
|
||
})
|
||
}
|
||
|
||
// 实习经历
|
||
for (const intern of internshipList.value) {
|
||
if (!intern.id) continue
|
||
records.push({
|
||
moduleType: 'internship',
|
||
recordId: intern.id,
|
||
recordLabel: `${intern.companyName || ''}${intern.position ? '-' + intern.position : ''}-经历描述`,
|
||
originalContent: intern.description || [],
|
||
})
|
||
}
|
||
|
||
// 项目经历
|
||
for (const proj of projectList.value) {
|
||
if (!proj.id) continue
|
||
records.push({
|
||
moduleType: 'project',
|
||
recordId: proj.id,
|
||
recordLabel: `${proj.projectName || ''}${proj.role ? '-' + proj.role : ''}-经历描述`,
|
||
originalContent: proj.description || [],
|
||
})
|
||
}
|
||
|
||
// 竞赛经历
|
||
for (const comp of competitionList.value) {
|
||
if (!comp.id) continue
|
||
records.push({
|
||
moduleType: 'competition',
|
||
recordId: comp.id,
|
||
recordLabel: `${comp.competitionName || ''}${comp.award ? '-' + comp.award : ''}-经历描述`,
|
||
originalContent: comp.description || [],
|
||
})
|
||
}
|
||
|
||
return records
|
||
})
|
||
|
||
/** 点击一键修复按钮 — 打开全模块修复抽屉 */
|
||
function handleFixAll() {
|
||
showFixAllDrawer.value = true
|
||
}
|
||
|
||
/** 将 DescriptionParagraph[] 中所有 text 拼接为字符串,用于内容对比 */
|
||
function joinDescriptionText(desc: DescriptionParagraph[]): string {
|
||
return desc.map(d => d.text || '').join('\n')
|
||
}
|
||
|
||
/** 一键修复 - 保存当前修改(只更新有变化的模块) */
|
||
async function handleFixAllSave(items: FixAllSubmitItem[]) {
|
||
if (!items.length) return
|
||
|
||
fullLoadingText.value = '正在保存修改…'
|
||
fullLoading.value = true
|
||
|
||
try {
|
||
for (const item of items) {
|
||
// 检查内容是否与简历原内容一致
|
||
const hasChanged = checkContentChanged(item)
|
||
if (!hasChanged) continue
|
||
|
||
// 更新简历内容
|
||
await updateModuleContent(item)
|
||
// 上报 resolve
|
||
await resolveDiagnosisIssue(item.issueId)
|
||
}
|
||
|
||
// 刷新数据
|
||
await Promise.all([loadResumeDetail(), loadDiagnosis()])
|
||
ElMessage.success('修改已保存')
|
||
} catch {
|
||
ElMessage.error('保存失败,请稍后重试')
|
||
} finally {
|
||
fullLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 一键使用AI润色结果 — 所有模块强制更新 */
|
||
async function handleFixAllUseAi(items: FixAllSubmitItem[]) {
|
||
if (!items.length) return
|
||
|
||
fullLoadingText.value = '正在应用AI润色结果…'
|
||
fullLoading.value = true
|
||
|
||
try {
|
||
for (const item of items) {
|
||
// 强制更新简历内容
|
||
await updateModuleContent(item)
|
||
// 所有 issue 都要上报 resolve
|
||
await resolveDiagnosisIssue(item.issueId)
|
||
}
|
||
|
||
// 刷新数据
|
||
await Promise.all([loadResumeDetail(), loadDiagnosis()])
|
||
ElMessage.success('AI润色结果已全部应用')
|
||
} catch {
|
||
ElMessage.error('操作失败,请稍后重试')
|
||
} finally {
|
||
fullLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 检查提交的内容是否与简历当前内容不同 */
|
||
function checkContentChanged(item: FixAllSubmitItem): boolean {
|
||
if (item.moduleType === 'summary') {
|
||
// summary 直接对比字符串
|
||
const current = resumeMain.value.summary || ''
|
||
return current.trim() !== (item.newContent as string).trim()
|
||
}
|
||
|
||
// 经历模块:找到对应记录,对比 description 文本
|
||
const listMap: Record<string, any[]> = {
|
||
education: educationList.value,
|
||
work: workList.value,
|
||
internship: internshipList.value,
|
||
project: projectList.value,
|
||
competition: competitionList.value,
|
||
}
|
||
const list = listMap[item.moduleType]
|
||
if (!list) return false
|
||
|
||
const record = list.find((r: any) => r.id === item.recordId)
|
||
if (!record?.description) return true
|
||
|
||
const currentText = joinDescriptionText(record.description)
|
||
const newText = joinDescriptionText(item.newContent as DescriptionParagraph[])
|
||
return currentText.trim() !== newText.trim()
|
||
}
|
||
|
||
/** 根据模块类型更新简历内容 */
|
||
async function updateModuleContent(item: FixAllSubmitItem) {
|
||
if (item.moduleType === 'summary') {
|
||
// 更新个人概述
|
||
const newSummary = item.newContent as string
|
||
resumeMain.value.summary = newSummary
|
||
await saveResumeMain({
|
||
resumeId,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: resumeMain.value.name || '',
|
||
email: resumeMain.value.email || '',
|
||
mobileNumber: resumeMain.value.mobileNumber || '',
|
||
city: resumeMain.value.city || '',
|
||
regionCode: resumeMain.value.regionCode || '',
|
||
wechatNumber: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||
skills: resumeMain.value.skills || [],
|
||
certificates: resumeMain.value.certificates || [],
|
||
summary: newSummary,
|
||
})
|
||
return
|
||
}
|
||
|
||
// 经历模块:找到记录并更新 description
|
||
const listMap: Record<string, any[]> = {
|
||
education: educationList.value,
|
||
work: workList.value,
|
||
internship: internshipList.value,
|
||
project: projectList.value,
|
||
competition: competitionList.value,
|
||
}
|
||
const list = listMap[item.moduleType]
|
||
if (!list) return
|
||
|
||
const record = list.find((r: any) => r.id === item.recordId)
|
||
if (record) {
|
||
// 替换 description
|
||
record.description = item.newContent as DescriptionParagraph[]
|
||
// 调用对应模块的编辑接口
|
||
await updateSingleRecord(item.moduleType, record)
|
||
}
|
||
}
|
||
|
||
/** 修复抽屉提交新版本 — 更新本地数据、调用保存接口、刷新页面 */
|
||
async function handleFixSubmit(content: string[]) {
|
||
fullLoadingText.value = '正在保存修改…'
|
||
fullLoading.value = true
|
||
|
||
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,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: resumeMain.value.name || '',
|
||
email: resumeMain.value.email || '',
|
||
mobileNumber: resumeMain.value.mobileNumber || '',
|
||
city: resumeMain.value.city || '',
|
||
regionCode: resumeMain.value.regionCode || '',
|
||
wechatNumber: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||
skills: resumeMain.value.skills || [],
|
||
certificates: resumeMain.value.certificates || [],
|
||
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 {
|
||
fullLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 根据模块类型调用对应的单条编辑接口(问题修复场景,只更新一条经历的内容) */
|
||
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,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ==================== 单条经历内联编辑相关 ====================
|
||
|
||
/** 当前正在编辑的经历记录 ID */
|
||
const expEditingId = ref('')
|
||
|
||
/** 当前正在新增经历的模块类型 */
|
||
const expAddingModule = ref('')
|
||
|
||
/** 开始编辑某条经历 */
|
||
function startEditExp(_moduleType: string, record: any) {
|
||
expEditingId.value = record.id
|
||
expAddingModule.value = ''
|
||
}
|
||
|
||
/** 开始添加新经历 */
|
||
function startAddExp(moduleType: string) {
|
||
expAddingModule.value = moduleType
|
||
expEditingId.value = ''
|
||
}
|
||
|
||
/** 保存编辑的经历(更新) */
|
||
async function handleExpUpdate(moduleType: string, recordId: string, data: Record<string, any>) {
|
||
try {
|
||
await updateSingleRecord(moduleType, { ...data, id: recordId })
|
||
await loadResumeDetail()
|
||
expEditingId.value = ''
|
||
ElMessage.success('保存成功')
|
||
} catch {
|
||
ElMessage.error('保存失败')
|
||
}
|
||
}
|
||
|
||
/** 删除单条经历 */
|
||
async function handleExpDelete(moduleType: string, recordId: string) {
|
||
try {
|
||
await ElMessageBox.confirm('确定要删除这条经历吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
const deleteMap: Record<string, (id: string) => Promise<any>> = {
|
||
education: deleteResumeEducation,
|
||
work: deleteResumeWork,
|
||
internship: deleteResumeInternship,
|
||
project: deleteResumeProject,
|
||
competition: deleteResumeCompetition,
|
||
}
|
||
const deleteFn = deleteMap[moduleType]
|
||
if (deleteFn) {
|
||
await deleteFn(recordId)
|
||
await loadResumeDetail()
|
||
ElMessage.success('删除成功')
|
||
}
|
||
} catch {
|
||
|
||
}
|
||
}
|
||
|
||
/** 保存新增的经历 */
|
||
async function handleExpAdd(moduleType: string, data: Record<string, any>) {
|
||
try {
|
||
const addMap: Record<string, (id: string, d: any) => Promise<any>> = {
|
||
education: addResumeEducation,
|
||
work: addResumeWork,
|
||
internship: addResumeInternship,
|
||
project: addResumeProject,
|
||
competition: addResumeCompetition,
|
||
}
|
||
const addFn = addMap[moduleType]
|
||
if (addFn) {
|
||
await addFn(resumeId, data)
|
||
await loadResumeDetail()
|
||
expAddingModule.value = ''
|
||
ElMessage.success('添加成功')
|
||
}
|
||
} catch {
|
||
ElMessage.error('添加失败')
|
||
}
|
||
}
|
||
|
||
// ==================== 编辑抽屉相关 ====================
|
||
|
||
/** 编辑抽屉的显示状态 */
|
||
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: resumeMain.value.regionCode || '',
|
||
wechat: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||
}
|
||
} 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,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: data.name,
|
||
email: data.email,
|
||
mobileNumber: data.phone,
|
||
city: data.locationName || data.location || '',
|
||
regionCode: data.location || '',
|
||
wechatNumber: data.wechat,
|
||
portfolioUrl: data.portfolioUrl,
|
||
skills: resumeMain.value.skills || [],
|
||
certificates: resumeMain.value.certificates || [],
|
||
summary: resumeMain.value.summary || '',
|
||
})
|
||
resumeMain.value.name = data.name
|
||
resumeMain.value.email = data.email
|
||
resumeMain.value.mobileNumber = data.phone
|
||
resumeMain.value.city = data.locationName || data.location || ''
|
||
resumeMain.value.regionCode = data.location || ''
|
||
resumeMain.value.wechatNumber = data.wechat
|
||
resumeMain.value.portfolioUrl = data.portfolioUrl
|
||
} else if (editModule.value === 'summary') {
|
||
// 保存个人概述到简历主表 — 传入完整主表字段
|
||
await saveResumeMain({
|
||
resumeId,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: resumeMain.value.name || '',
|
||
email: resumeMain.value.email || '',
|
||
mobileNumber: resumeMain.value.mobileNumber || '',
|
||
city: resumeMain.value.city || '',
|
||
regionCode: resumeMain.value.regionCode || '',
|
||
wechatNumber: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||
skills: resumeMain.value.skills || [],
|
||
certificates: resumeMain.value.certificates || [],
|
||
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,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: resumeMain.value.name || '',
|
||
email: resumeMain.value.email || '',
|
||
mobileNumber: resumeMain.value.mobileNumber || '',
|
||
city: resumeMain.value.city || '',
|
||
regionCode: resumeMain.value.regionCode || '',
|
||
wechatNumber: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: data.portfolioUrl,
|
||
skills: resumeMain.value.skills || [],
|
||
certificates: resumeMain.value.certificates || [],
|
||
summary: resumeMain.value.summary || '',
|
||
})
|
||
resumeMain.value.portfolioUrl = data.portfolioUrl
|
||
} else if (editModule.value === 'skills') {
|
||
// 保存技能到简历主表 — 传入完整主表字段
|
||
await saveResumeMain({
|
||
resumeId,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: resumeMain.value.name || '',
|
||
email: resumeMain.value.email || '',
|
||
mobileNumber: resumeMain.value.mobileNumber || '',
|
||
city: resumeMain.value.city || '',
|
||
regionCode: resumeMain.value.regionCode || '',
|
||
wechatNumber: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||
skills: [...data.skills],
|
||
certificates: resumeMain.value.certificates || [],
|
||
summary: resumeMain.value.summary || '',
|
||
})
|
||
resumeMain.value.skills = [...data.skills]
|
||
} else if (editModule.value === 'certificate') {
|
||
// 保存证书到简历主表 — 传入完整主表字段
|
||
await saveResumeMain({
|
||
resumeId,
|
||
resumeName: resumeMain.value.resumeName,
|
||
targetPosition: resumeMain.value.targetPosition,
|
||
name: resumeMain.value.name || '',
|
||
email: resumeMain.value.email || '',
|
||
mobileNumber: resumeMain.value.mobileNumber || '',
|
||
city: resumeMain.value.city || '',
|
||
regionCode: resumeMain.value.regionCode || '',
|
||
wechatNumber: resumeMain.value.wechatNumber || '',
|
||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||
skills: resumeMain.value.skills || [],
|
||
certificates: [...data.certificates],
|
||
summary: resumeMain.value.summary || '',
|
||
})
|
||
resumeMain.value.certificates = [...data.certificates]
|
||
}
|
||
|
||
// 保存成功后重新加载简历数据,确保 ID 等字段与服务端一致
|
||
await loadResumeDetail()
|
||
} catch {
|
||
console.error('[ResumeDetail] 保存失败')
|
||
}
|
||
}
|
||
</script>
|