AI助手和Nova助手

This commit is contained in:
2026-05-06 15:07:44 +08:00
parent 1c91818494
commit f341408254
38 changed files with 4759 additions and 657 deletions
+662 -391
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -266,7 +266,7 @@
</div>
<AiChat />
<AiChat :job-id="jobId" />
<!-- 职位不感兴趣反馈弹窗 -->
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
+10 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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] 保存失败')
}