AI助手和Nova助手
This commit is contained in:
+662
-391
File diff suppressed because it is too large
Load Diff
@@ -266,7 +266,7 @@
|
||||
|
||||
|
||||
</div>
|
||||
<AiChat />
|
||||
<AiChat :job-id="jobId" />
|
||||
|
||||
<!-- 职位不感兴趣反馈弹窗 -->
|
||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
|
||||
|
||||
+10
-2
@@ -176,7 +176,7 @@
|
||||
</div>
|
||||
|
||||
<div class="jobs-page__job-action-right ">
|
||||
<button class="jobs-page__job-helper ml5">
|
||||
<button class="jobs-page__job-helper ml5" @click.stop="askAssistant(job)">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__helper-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M5.5 5.5a1.5 1.5 0 113 0c0 .8-.7 1-1.5 1.5V9" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
@@ -239,7 +239,7 @@
|
||||
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多了</div>
|
||||
</div>
|
||||
</div>
|
||||
<AiChat />
|
||||
<AiChat :job-id="currentAskJobId" />
|
||||
|
||||
<!-- 职位不感兴趣反馈弹窗 -->
|
||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
||||
@@ -299,6 +299,14 @@ const showFeedbackDialog = ref(false)
|
||||
/** 当前操作的职位 ID(用于提交问题反馈) */
|
||||
const feedbackJobId = ref<string | null>(null)
|
||||
|
||||
/** 当前问助手的岗位 ID(传给 AiChat 组件) */
|
||||
const currentAskJobId = ref<string>('')
|
||||
|
||||
/** 点击"问助手"按钮,传入岗位 ID 给 AiChat */
|
||||
function askAssistant(job: JobItem) {
|
||||
currentAskJobId.value = job.id
|
||||
}
|
||||
|
||||
// ==================== 收藏统计 ====================
|
||||
|
||||
/** 收藏总数(用于 Tab 标签显示) */
|
||||
|
||||
+131
-8
@@ -73,15 +73,53 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 编辑简历名称弹窗 -->
|
||||
<ResumeEditNameDialog
|
||||
v-model="editNameVisible"
|
||||
:resume-id="editResumeId"
|
||||
:resume-name="editResumeName"
|
||||
:target-position="editTargetPosition"
|
||||
@saved="loadResumeList"
|
||||
/>
|
||||
|
||||
<!-- 导出简历格式选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="exportDialogVisible"
|
||||
title="导出简历"
|
||||
width="3.6rem"
|
||||
:close-on-click-modal="false"
|
||||
class="resume-export-dialog"
|
||||
>
|
||||
<!-- 格式选择 -->
|
||||
<el-radio-group v-model="exportFormat" class="resume-export-dialog__radio-group">
|
||||
<el-radio value="pdf">PDF 简历</el-radio>
|
||||
<el-radio value="word">Word 简历</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<el-button @click="exportDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="exporting" @click="doExport">下载</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 隐藏的简历模板,用于导出时渲染DOM -->
|
||||
<div v-if="exportTemplateData" style="position:absolute;left:-9999px;top:0;">
|
||||
<JobResumeTemplate ref="exportTemplateRef" :resume-data="exportTemplateData" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import { exportResumePdf, exportResumeWord, loadResumeTemplateData } from '@/utils/resumeExport'
|
||||
import { uploadResume } from '@/utils/aiRequest'
|
||||
import { fetchResumeList, deleteResume, type ResumeListItem } from '@/api/resume'
|
||||
import {
|
||||
fetchResumeList, deleteResume, type ResumeListItem,
|
||||
} from '@/api/resume'
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||
// ElLoading.service() 是命令式调用,按需引入插件不会自动加载其样式,需手动引入
|
||||
import 'element-plus/es/components/loading/style/css'
|
||||
@@ -110,12 +148,12 @@ interface ResumeItem {
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 将 Instant 时间戳转为友好的相对时间文案
|
||||
* @param instant 后端返回的 Instant 对象
|
||||
* 将毫秒时间戳转为友好的相对时间文案
|
||||
* @param timestamp 后端返回的毫秒级时间戳
|
||||
*/
|
||||
function formatTime(instant?: { seconds?: number; nanos?: number }): string {
|
||||
if (!instant?.seconds) return '-'
|
||||
const date = new Date(instant.seconds * 1000)
|
||||
function formatTime(timestamp?: number): string {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp)
|
||||
const now = Date.now()
|
||||
const diff = now - date.getTime()
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
@@ -175,8 +213,78 @@ onMounted(() => {
|
||||
/** 当前打开弹出菜单的简历 ID */
|
||||
const activeMenuId = ref<string | null>(null)
|
||||
|
||||
// ==================== 编辑简历名称弹窗状态 ====================
|
||||
|
||||
/** 编辑弹窗是否可见 */
|
||||
const editNameVisible = ref(false)
|
||||
/** 当前编辑的简历 ID */
|
||||
const editResumeId = ref('')
|
||||
/** 当前编辑的简历名称 */
|
||||
const editResumeName = ref('')
|
||||
/** 当前编辑的目标岗位 */
|
||||
const editTargetPosition = ref('')
|
||||
|
||||
// ==================== 导出简历弹窗状态 ====================
|
||||
|
||||
/** 导出弹窗是否可见 */
|
||||
const exportDialogVisible = ref(false)
|
||||
/** 导出格式:pdf 或 word */
|
||||
const exportFormat = ref<'pdf' | 'word'>('pdf')
|
||||
/** 导出中状态 */
|
||||
const exporting = ref(false)
|
||||
/** 当前导出的简历 ID */
|
||||
const exportResumeId = ref('')
|
||||
/** 当前导出的简历名称(用于文件名) */
|
||||
const exportResumeName = ref('')
|
||||
/** 导出用的简历模板数据 */
|
||||
const exportTemplateData = ref<ResumeTemplateData | null>(null)
|
||||
/** 导出用的简历模板组件引用 */
|
||||
const exportTemplateRef = ref<InstanceType<typeof JobResumeTemplate> | null>(null)
|
||||
|
||||
/** 执行导出下载 */
|
||||
async function doExport() {
|
||||
exporting.value = true
|
||||
try {
|
||||
// 1. 加载简历完整数据
|
||||
const data = await loadResumeTemplateData(exportResumeId.value)
|
||||
if (!data) {
|
||||
ElMessage.error('获取简历数据失败')
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 设置模板数据,等待DOM渲染
|
||||
exportTemplateData.value = data
|
||||
await nextTick()
|
||||
|
||||
// 3. 获取渲染后的DOM
|
||||
const element = exportTemplateRef.value?.resumeRef
|
||||
if (!element) {
|
||||
ElMessage.error('简历模板渲染失败')
|
||||
return
|
||||
}
|
||||
|
||||
const fileName = exportResumeName.value || '简历'
|
||||
|
||||
if (exportFormat.value === 'pdf') {
|
||||
await exportResumePdf(element, fileName)
|
||||
} else {
|
||||
exportResumeWord(element, fileName)
|
||||
}
|
||||
|
||||
ElMessage.success('导出成功')
|
||||
exportDialogVisible.value = false
|
||||
} catch (err) {
|
||||
console.error('[导出简历] 失败', err)
|
||||
ElMessage.error('导出失败,请稍后重试')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
// 清理隐藏模板数据
|
||||
exportTemplateData.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 弹出菜单操作项 */
|
||||
const popupActions = ['设为默认简历', '编辑', '导出简历', '删除']
|
||||
const popupActions = ['设为默认简历', '编辑名称岗位', '导出简历', '删除']
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
@@ -213,6 +321,21 @@ async function handleAction(action: string, id: string) {
|
||||
ElMessage.error('删除失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
} else if (action === '编辑名称岗位') {
|
||||
// 找到当前简历数据,打开编辑弹窗
|
||||
const target = resumeList.value.find(r => r.id === id)
|
||||
if (target) {
|
||||
editResumeId.value = id
|
||||
editResumeName.value = target.name
|
||||
editTargetPosition.value = target.targetJob
|
||||
editNameVisible.value = true
|
||||
}
|
||||
} else if (action === '导出简历') {
|
||||
// 打开导出格式选择弹窗
|
||||
exportResumeId.value = id
|
||||
const target = resumeList.value.find(r => r.id === id)
|
||||
exportResumeName.value = target?.name || '简历'
|
||||
exportDialogVisible.value = true
|
||||
} else {
|
||||
console.log(action, id)
|
||||
}
|
||||
|
||||
+309
-101
@@ -69,7 +69,7 @@
|
||||
<div class="resume-detail__score-bar">
|
||||
<div class="resume-detail__score-left">
|
||||
<!-- 评级 -->
|
||||
<span class="resume-detail__score-avatar">
|
||||
<span v-if="hasDiagnosis" class="resume-detail__score-avatar">
|
||||
{{ diagnosisReport.grade }}
|
||||
</span>
|
||||
<!-- 有诊断报告时显示评级和查看链接 -->
|
||||
@@ -82,15 +82,18 @@
|
||||
<!-- 有诊断报告时显示三项计数和重新诊断按钮 -->
|
||||
<template v-if="hasDiagnosis">
|
||||
<div class="resume-detail__score-item">
|
||||
<span class="resume-detail__score-num">{{ diagnosisReport.urgentTotal || 0 }}</span>
|
||||
<!-- 从 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">
|
||||
<span class="resume-detail__score-num">{{ diagnosisReport.importantTotal || 0 }}</span>
|
||||
<!-- 从 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">
|
||||
<span class="resume-detail__score-num">{{ diagnosisReport.expressionTotal || 0 }}</span>
|
||||
<!-- 从 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>
|
||||
@@ -189,7 +192,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 教育背景 -->
|
||||
<!-- 教育背景经历 -->
|
||||
<div v-if="educationList.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">教育背景</h3>
|
||||
@@ -414,11 +417,22 @@ import {
|
||||
fetchResumeDiagnosis,
|
||||
triggerResumeDiagnosis,
|
||||
saveResumeMain,
|
||||
saveResumeEducation,
|
||||
saveResumeWork,
|
||||
saveResumeInternship,
|
||||
saveResumeProject,
|
||||
saveResumeCompetition,
|
||||
addResumeEducation,
|
||||
updateResumeEducation,
|
||||
deleteResumeEducation,
|
||||
addResumeWork,
|
||||
updateResumeWork,
|
||||
deleteResumeWork,
|
||||
addResumeInternship,
|
||||
updateResumeInternship,
|
||||
deleteResumeInternship,
|
||||
addResumeProject,
|
||||
updateResumeProject,
|
||||
deleteResumeProject,
|
||||
addResumeCompetition,
|
||||
updateResumeCompetition,
|
||||
deleteResumeCompetition,
|
||||
deleteResume,
|
||||
type ResumeMainData,
|
||||
type ResumeEducation,
|
||||
type ResumeWork,
|
||||
@@ -450,6 +464,36 @@ const gradeLabel = computed(() => {
|
||||
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>>({})
|
||||
|
||||
@@ -458,11 +502,37 @@ function getIssuesByModule(moduleType: string): DiagnosisIssue[] {
|
||||
return diagnosisIssues.value.filter(i => i.moduleType === moduleType)
|
||||
}
|
||||
|
||||
/** 根据模块类型和记录 ID 获取对应的 issue */
|
||||
/** 根据模块类型和列表索引获取对应的 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
|
||||
@@ -614,8 +684,30 @@ function handleEdit() { console.log('编辑简历信息') }
|
||||
/** 导出简历 */
|
||||
function handleExport() { console.log('导出') }
|
||||
|
||||
/** 删除简历 */
|
||||
function handleDelete() { console.log('删除') }
|
||||
/** 删除简历 — 二次确认后调用删除接口,成功后返回简历列表 */
|
||||
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() {
|
||||
@@ -741,8 +833,8 @@ async function handleFixSubmit(content: string[]) {
|
||||
record.description[idx].text = text
|
||||
}
|
||||
})
|
||||
// 调用对应模块的保存接口(全量覆盖)
|
||||
await saveModuleData(moduleType)
|
||||
// 调用对应模块的单条编辑接口(仅更新当前这条经历)
|
||||
await updateSingleRecord(moduleType, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -757,50 +849,55 @@ async function handleFixSubmit(content: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据模块类型调用对应的保存接口(全量覆盖) */
|
||||
async function saveModuleData(moduleType: string) {
|
||||
/** 根据模块类型调用对应的单条编辑接口(问题修复场景,只更新一条经历的内容) */
|
||||
async function updateSingleRecord(moduleType: string, record: any) {
|
||||
if (moduleType === 'education') {
|
||||
await saveResumeEducation(resumeId, educationList.value.map(edu => ({
|
||||
school: edu.school,
|
||||
major: edu.major,
|
||||
degree: edu.degree,
|
||||
studyType: edu.studyType,
|
||||
startDate: edu.startDate,
|
||||
endDate: edu.endDate,
|
||||
description: edu.description,
|
||||
})))
|
||||
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 saveResumeWork(resumeId, workList.value.map(w => ({
|
||||
companyName: w.companyName,
|
||||
position: w.position,
|
||||
startDate: w.startDate,
|
||||
endDate: w.endDate,
|
||||
description: w.description,
|
||||
})))
|
||||
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 saveResumeInternship(resumeId, internshipList.value.map(i => ({
|
||||
companyName: i.companyName,
|
||||
position: i.position,
|
||||
startDate: i.startDate,
|
||||
endDate: i.endDate,
|
||||
description: i.description,
|
||||
})))
|
||||
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 saveResumeProject(resumeId, projectList.value.map(p => ({
|
||||
projectName: p.projectName,
|
||||
companyName: p.companyName,
|
||||
role: p.role,
|
||||
startDate: p.startDate,
|
||||
endDate: p.endDate,
|
||||
description: p.description,
|
||||
})))
|
||||
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 saveResumeCompetition(resumeId, competitionList.value.map(c => ({
|
||||
competitionName: c.competitionName,
|
||||
award: c.award,
|
||||
awardDate: c.awardDate,
|
||||
description: c.description,
|
||||
})))
|
||||
await updateResumeCompetition({
|
||||
id: record.id,
|
||||
competitionName: record.competitionName,
|
||||
award: record.award,
|
||||
awardDate: record.awardDate,
|
||||
description: record.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,6 +912,21 @@ 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
|
||||
@@ -832,8 +944,11 @@ function openEditDrawer(section: string) {
|
||||
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 || '全日制',
|
||||
@@ -844,8 +959,11 @@ function openEditDrawer(section: string) {
|
||||
})),
|
||||
}
|
||||
} 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 || '',
|
||||
@@ -854,8 +972,11 @@ function openEditDrawer(section: string) {
|
||||
})),
|
||||
}
|
||||
} 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 || '',
|
||||
@@ -864,8 +985,11 @@ function openEditDrawer(section: string) {
|
||||
})),
|
||||
}
|
||||
} 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 || '',
|
||||
@@ -875,8 +999,11 @@ function openEditDrawer(section: string) {
|
||||
})),
|
||||
}
|
||||
} 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 || '',
|
||||
@@ -926,57 +1053,135 @@ async function handleSaveEdit(data: Record<string, any>) {
|
||||
})
|
||||
resumeMain.value.summary = data.summary
|
||||
} else if (editModule.value === 'education') {
|
||||
// 保存教育经历
|
||||
await saveResumeEducation(resumeId, data.education.map((edu: any) => ({
|
||||
school: edu.school,
|
||||
major: edu.major,
|
||||
degree: edu.degree,
|
||||
studyType: edu.studyType,
|
||||
startDate: edu.startDate,
|
||||
endDate: edu.endDate,
|
||||
description: edu.description,
|
||||
})))
|
||||
educationList.value = data.education.map((edu: any) => ({ ...edu, description: edu.description.map((d: any) => ({ ...d })) }))
|
||||
// 教育经历 — 通过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') {
|
||||
// 保存工作经历
|
||||
await saveResumeWork(resumeId, data.works.map((w: any) => ({
|
||||
companyName: w.companyName,
|
||||
position: w.position,
|
||||
startDate: w.startDate,
|
||||
endDate: w.endDate,
|
||||
description: w.description,
|
||||
})))
|
||||
workList.value = data.works.map((w: any) => ({ ...w, description: w.description.map((d: any) => ({ ...d })) }))
|
||||
// 工作经历 — 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') {
|
||||
// 保存实习经历
|
||||
await saveResumeInternship(resumeId, data.internships.map((i: any) => ({
|
||||
companyName: i.companyName,
|
||||
position: i.position,
|
||||
startDate: i.startDate,
|
||||
endDate: i.endDate,
|
||||
description: i.description,
|
||||
})))
|
||||
internshipList.value = data.internships.map((i: any) => ({ ...i, description: i.description.map((d: any) => ({ ...d })) }))
|
||||
// 实习经历 — 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') {
|
||||
// 保存项目经历
|
||||
await saveResumeProject(resumeId, data.projects.map((p: any) => ({
|
||||
projectName: p.projectName,
|
||||
companyName: p.companyName,
|
||||
role: p.role,
|
||||
startDate: p.startDate,
|
||||
endDate: p.endDate,
|
||||
description: p.description,
|
||||
})))
|
||||
projectList.value = data.projects.map((p: any) => ({ ...p, description: p.description.map((d: any) => ({ ...d })) }))
|
||||
// 项目经历 — 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') {
|
||||
// 保存竞赛经历
|
||||
await saveResumeCompetition(resumeId, data.competitions.map((c: any) => ({
|
||||
competitionName: c.competitionName,
|
||||
award: c.award,
|
||||
awardDate: c.awardDate,
|
||||
description: c.description,
|
||||
})))
|
||||
competitionList.value = data.competitions.map((c: any) => ({ ...c, description: c.description.map((d: any) => ({ ...d })) }))
|
||||
// 竞赛经历 — 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({
|
||||
@@ -999,6 +1204,9 @@ async function handleSaveEdit(data: Record<string, any>) {
|
||||
})
|
||||
resumeMain.value.certificates = [...data.certificates]
|
||||
}
|
||||
|
||||
// 保存成功后重新加载简历数据,确保 ID 等字段与服务端一致
|
||||
await loadResumeDetail()
|
||||
} catch {
|
||||
console.error('[ResumeDetail] 保存失败')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user