Files
offerpai_web/src/components/JobResumeTemplate.vue
T

502 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 简历HTML模板组件用于预览和后续导出PDF/Word -->
<div class="job-resume-template" ref="resumeRef">
<div class="resume-html" :class="{ 'resume-html--export': props.mode === 'export' }">
<!-- 姓名 -->
<h1 class="resume-html__name">
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(oldResumeData?.name, resumeData.name)" :key="'name-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ resumeData.name }}</template>
</h1>
<!-- 联系方式 -->
<div class="resume-html__contact">
<span v-if="resumeData.email">邮箱{{ resumeData.email }}</span>
<div class="resume-html__contact-row">
<span v-if="resumeData.mobileNumber">手机{{ resumeData.mobileNumber }}</span>
<span v-if="resumeData.wechatNumber" class="resume-html__separator"></span>
<span v-if="resumeData.wechatNumber">微信号{{ resumeData.wechatNumber }}</span>
</div>
</div>
<!-- 个人概述 -->
<template v-if="resumeData.summary">
<div class="resume-html__section-title">个人概述</div>
<div class="resume-html__divider"></div>
<div class="resume-html__summary" :class="{ 'resume-html__diff-block': isSummaryDiffBlock() }">
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(oldResumeData?.summary, resumeData.summary)" :key="'sum-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ resumeData.summary }}</template>
</div>
</template>
<!-- 教育背景 -->
<template v-if="resumeData.educations && resumeData.educations.length">
<div class="resume-html__section-title">教育背景</div>
<div class="resume-html__divider"></div>
<div v-for="(edu, idx) in resumeData.educations" :key="'edu-' + idx" class="resume-html__item" :class="{ 'resume-html__diff-block': isEduDiffBlock(idx) }">
<div class="resume-html__item-header">
<span class="resume-html__item-main">
<!-- 教育经历标题差异对比 -->
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(
oldResumeData?.educations?.[idx] ? (oldResumeData.educations[idx].school + '' + oldResumeData.educations[idx].major + '' + degreeText(oldResumeData.educations[idx].degree)) : '',
edu.school + '' + edu.major + '' + degreeText(edu.degree)
)" :key="'eduh-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ edu.school }}{{ edu.major }}{{ degreeText(edu.degree) }}</template>
</span>
<div class="resume-html__item-right">
<span class="resume-html__item-location" v-if="edu.location">{{ edu.location }}</span>
<span class="resume-html__item-date">{{ edu.startDate }} — {{ edu.endDate || '至今' }}</span>
</div>
</div>
<!-- 教育经历描述独立一行,避免被header的flex布局挤压 -->
<div v-if="edu.description && edu.description.length" class="resume-html__item-desc">
<template v-if="showDiff">
主修课程:<template v-for="(desc, di) in edu.description" :key="'ed-' + di"><template v-if="di > 0">、</template><template v-for="(seg, si) in diffDescText(oldResumeData?.educations, idx, di, desc.text)" :key="'eds-' + si"><span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template></template></template>等;
</template>
<template v-else>
主修课程:{{ edu.description.map(d => d.text).join('、') }}等;
</template>
</div>
</div>
</template>
<!-- 工作经历 -->
<template v-if="resumeData.workExperiences && resumeData.workExperiences.length">
<div class="resume-html__section-title">工作经历</div>
<div class="resume-html__divider"></div>
<div v-for="(work, idx) in resumeData.workExperiences" :key="'work-' + idx" class="resume-html__item" :class="{ 'resume-html__diff-block': isWorkDiffBlock(idx) }">
<div class="resume-html__item-header">
<span class="resume-html__item-main">
<!-- 工作经历标题差异对比 -->
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(
oldResumeData?.workExperiences?.[idx] ? (oldResumeData.workExperiences[idx].companyName + '' + oldResumeData.workExperiences[idx].position) : '',
work.companyName + '' + work.position
)" :key="'workh-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ work.companyName }}{{ work.position }}</template>
</span>
<div class="resume-html__item-right">
<span class="resume-html__item-location" v-if="work.location">{{ work.location }}</span>
<span class="resume-html__item-date">{{ work.startDate }} — {{ work.endDate || '至今' }}</span>
</div>
</div>
<ul v-if="work.description && work.description.length" class="resume-html__desc-list">
<li v-for="(desc, di) in work.description" :key="'wd-' + di">
<template v-if="showDiff">
<template v-for="(seg, si) in diffListText(oldResumeData?.workExperiences, idx, di, desc.text)" :key="'wds-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ desc.text }}</template>
</li>
</ul>
</div>
</template>
<!-- 实习经历 -->
<template v-if="resumeData.internships && resumeData.internships.length">
<div class="resume-html__section-title">实习经历</div>
<div class="resume-html__divider"></div>
<div v-for="(intern, idx) in resumeData.internships" :key="'intern-' + idx" class="resume-html__item" :class="{ 'resume-html__diff-block': isInternDiffBlock(idx) }">
<div class="resume-html__item-header">
<span class="resume-html__item-main">
<!-- 实习经历标题差异对比 -->
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(
oldResumeData?.internships?.[idx] ? (oldResumeData.internships[idx].companyName + '' + oldResumeData.internships[idx].position) : '',
intern.companyName + '' + intern.position
)" :key="'internh-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ intern.companyName }}{{ intern.position }}</template>
</span>
<div class="resume-html__item-right">
<span class="resume-html__item-location" v-if="intern.location">{{ intern.location }}</span>
<span class="resume-html__item-date">{{ intern.startDate }} — {{ intern.endDate || '至今' }}</span>
</div>
</div>
<ul v-if="intern.description && intern.description.length" class="resume-html__desc-list">
<li v-for="(desc, di) in intern.description" :key="'id-' + di">
<template v-if="showDiff">
<template v-for="(seg, si) in diffListText(oldResumeData?.internships, idx, di, desc.text)" :key="'ids-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ desc.text }}</template>
</li>
</ul>
</div>
</template>
<!-- 项目经历 -->
<template v-if="resumeData.projects && resumeData.projects.length">
<div class="resume-html__section-title">项目经历</div>
<div class="resume-html__divider"></div>
<div v-for="(proj, idx) in resumeData.projects" :key="'proj-' + idx" class="resume-html__item" :class="{ 'resume-html__diff-block': isProjDiffBlock(idx) }">
<div class="resume-html__item-header">
<span class="resume-html__item-main">
<!-- 项目经历标题差异对比 -->
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(
oldResumeData?.projects?.[idx] ? (oldResumeData.projects[idx].projectName + (oldResumeData.projects[idx].role ? '' + oldResumeData.projects[idx].role : '')) : '',
proj.projectName + (proj.role ? '' + proj.role : '')
)" :key="'projh-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ proj.projectName }}{{ proj.role ? '' + proj.role : '' }}</template>
</span>
<div class="resume-html__item-right">
<span class="resume-html__item-location" v-if="proj.companyName">
<!-- 项目所属公司差异对比 -->
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(getOldFieldText(oldResumeData?.projects, idx, 'companyName'), proj.companyName)" :key="'projc-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ proj.companyName }}</template>
</span>
<span class="resume-html__item-date">{{ proj.startDate }} — {{ proj.endDate || '至今' }}</span>
</div>
</div>
<ul v-if="proj.description && proj.description.length" class="resume-html__desc-list">
<li v-for="(desc, di) in proj.description" :key="'pd-' + di">
<template v-if="showDiff">
<template v-for="(seg, si) in diffListText(oldResumeData?.projects, idx, di, desc.text, 'projects')" :key="'pds-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ desc.text }}</template>
</li>
</ul>
</div>
</template>
<!-- 竞赛/获奖经历 -->
<template v-if="resumeData.competitions && resumeData.competitions.length">
<div class="resume-html__section-title">获奖经历</div>
<div class="resume-html__divider"></div>
<div v-for="(comp, idx) in resumeData.competitions" :key="'comp-' + idx" class="resume-html__item" :class="{ 'resume-html__diff-block': isCompDiffBlock(idx) }">
<div class="resume-html__item-header">
<span class="resume-html__item-main">
<!-- 竞赛经历标题差异对比 -->
<template v-if="showDiff">
<template v-for="(seg, si) in diffText(
oldResumeData?.competitions?.[idx] ? (oldResumeData.competitions[idx].competitionName + (oldResumeData.competitions[idx].award ? '' + oldResumeData.competitions[idx].award : '')) : '',
comp.competitionName + (comp.award ? '' + comp.award : '')
)" :key="'comph-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ comp.competitionName }}{{ comp.award ? '' + comp.award : '' }}</template>
</span>
<span class="resume-html__item-date" v-if="comp.awardDate">{{ comp.awardDate }}</span>
</div>
<ul v-if="comp.description && comp.description.length" class="resume-html__desc-list">
<li v-for="(desc, di) in comp.description" :key="'cd-' + di">
<template v-if="showDiff">
<template v-for="(seg, si) in diffListText(oldResumeData?.competitions, idx, di, desc.text, 'competitions')" :key="'cds-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</template>
<template v-else>{{ desc.text }}</template>
</li>
</ul>
</div>
</template>
<!-- 专业技能 -->
<template v-if="hasSkillsSection">
<div class="resume-html__section-title">专业技能</div>
<div class="resume-html__divider"></div>
<div class="resume-html__skills">
<div v-if="resumeData.skills && resumeData.skills.length" class="resume-html__skill-row">
<span class="resume-html__skill-label">技能:</span>
<template v-if="showDiff">
<span>
<template v-for="(seg, si) in diffText(oldResumeData?.skills?.join('、'), resumeData.skills.join('、'))" :key="'sk-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</span>
</template>
<span v-else>{{ resumeData.skills.join('、') }}</span>
</div>
<div v-if="resumeData.certificates && resumeData.certificates.length" class="resume-html__skill-row">
<span class="resume-html__skill-label">证书:</span>
<template v-if="showDiff">
<span>
<template v-for="(seg, si) in diffText(oldResumeData?.certificates?.join('、'), resumeData.certificates.join('、'))" :key="'ct-' + si">
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</span>
</template>
<span v-else>{{ resumeData.certificates.join('、') }}</span>
</div>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { computeDiff, type DiffSegment } from '@/utils/textDiff'
// ==================== 类型定义 ====================
/** 描述段落 */
interface DescParagraph {
id?: string
text: string
}
/** 教育经历 */
interface ResumeEducation {
school: string
major: string
degree: number
startDate: string
endDate: string
location?: string
description?: DescParagraph[]
}
/** 工作/实习经历 */
interface ResumeWork {
companyName: string
position: string
startDate: string
endDate?: string
location?: string
description?: DescParagraph[]
}
/** 项目经历 */
interface ResumeProject {
projectName: string
companyName?: string
role?: string
startDate: string
endDate?: string
description?: DescParagraph[]
}
/** 竞赛/获奖经历 */
interface ResumeCompetition {
competitionName: string
award?: string
awardDate?: string
description?: DescParagraph[]
}
/** 简历完整数据结构 */
export interface ResumeTemplateData {
/** 姓名 */
name: string
/** 邮箱 */
email?: string
/** 手机号 */
mobileNumber?: string
/** 微信号 */
wechatNumber?: string
/** 个人概述 */
summary?: string
/** 教育背景 */
educations?: ResumeEducation[]
/** 工作经历 */
workExperiences?: ResumeWork[]
/** 实习经历 */
internships?: ResumeWork[]
/** 项目经历 */
projects?: ResumeProject[]
/** 竞赛/获奖经历 */
competitions?: ResumeCompetition[]
/** 技能标签 */
skills?: string[]
/** 证书标签 */
certificates?: string[]
}
// ==================== Props ====================
const props = defineProps<{
/** 简历数据 */
resumeData: ResumeTemplateData
/** 是否显示差异对比模式默认false显示普通模板 */
showDiff?: boolean
/** 旧简历数据差异对比时使用与resumeData对比 */
oldResumeData?: ResumeTemplateData
/** 模式preview=预览小padding),export=导出大padding),默认preview */
mode?: 'preview' | 'export'
}>()
// ==================== DOM引用(供父组件获取HTML内容) ====================
const resumeRef = ref<HTMLElement>()
/** 暴露DOM引用,方便后续导出PDF/Word */
defineExpose({ resumeRef })
// ==================== 工具方法 ====================
/** 学历数字转文字 */
function degreeText(degree: number): string {
const map: Record<number, string> = { 1: '大专', 2: '本科', 3: '硕士', 4: '博士' }
return map[degree] || ''
}
/** 是否有技能相关内容 */
const hasSkillsSection = computed(() => {
return (props.resumeData.skills && props.resumeData.skills.length > 0) ||
(props.resumeData.certificates && props.resumeData.certificates.length > 0)
})
// ==================== 差异块背景判断方法 ====================
/** 个人概述是否有差异 */
function isSummaryDiffBlock(): boolean {
if (!props.showDiff || !props.oldResumeData) return false
return props.oldResumeData.summary !== props.resumeData.summary
}
/** 教育经历某项是否有差异 */
function isEduDiffBlock(idx: number): boolean {
if (!props.showDiff || !props.oldResumeData) return false
const oldEdu = props.oldResumeData.educations?.[idx]
const newEdu = props.resumeData.educations?.[idx]
if (!oldEdu || !newEdu) return true
if (oldEdu.school !== newEdu.school || oldEdu.major !== newEdu.major || oldEdu.degree !== newEdu.degree) return true
const oldDescs = oldEdu.description?.map(d => d.text).join('') || ''
const newDescs = newEdu.description?.map(d => d.text).join('') || ''
return oldDescs !== newDescs
}
/** 工作经历某项是否有差异 */
function isWorkDiffBlock(idx: number): boolean {
if (!props.showDiff || !props.oldResumeData) return false
const oldItem = props.oldResumeData.workExperiences?.[idx]
const newItem = props.resumeData.workExperiences?.[idx]
if (!oldItem || !newItem) return true
if (oldItem.companyName !== newItem.companyName || oldItem.position !== newItem.position) return true
const oldDescs = oldItem.description?.map(d => d.text).join('') || ''
const newDescs = newItem.description?.map(d => d.text).join('') || ''
return oldDescs !== newDescs
}
/** 实习经历某项是否有差异 */
function isInternDiffBlock(idx: number): boolean {
if (!props.showDiff || !props.oldResumeData) return false
const oldItem = props.oldResumeData.internships?.[idx]
const newItem = props.resumeData.internships?.[idx]
if (!oldItem || !newItem) return true
if (oldItem.companyName !== newItem.companyName || oldItem.position !== newItem.position) return true
const oldDescs = oldItem.description?.map(d => d.text).join('') || ''
const newDescs = newItem.description?.map(d => d.text).join('') || ''
return oldDescs !== newDescs
}
/** 项目经历某项是否有差异 */
function isProjDiffBlock(idx: number): boolean {
if (!props.showDiff || !props.oldResumeData) return false
const oldItem = props.oldResumeData.projects?.[idx]
const newItem = props.resumeData.projects?.[idx]
if (!oldItem || !newItem) return true
if (oldItem.projectName !== newItem.projectName || oldItem.role !== newItem.role || oldItem.companyName !== newItem.companyName) return true
const oldDescs = oldItem.description?.map(d => d.text).join('') || ''
const newDescs = newItem.description?.map(d => d.text).join('') || ''
return oldDescs !== newDescs
}
/** 竞赛经历某项是否有差异 */
function isCompDiffBlock(idx: number): boolean {
if (!props.showDiff || !props.oldResumeData) return false
const oldItem = props.oldResumeData.competitions?.[idx]
const newItem = props.resumeData.competitions?.[idx]
if (!oldItem || !newItem) return true
if (oldItem.competitionName !== newItem.competitionName || oldItem.award !== newItem.award) return true
const oldDescs = oldItem.description?.map(d => d.text).join('') || ''
const newDescs = newItem.description?.map(d => d.text).join('') || ''
return oldDescs !== newDescs
}
// ==================== 差异对比工具方法 ====================
/**
* 对比两段纯文本,返回差异片段数组
* @param oldText 旧文本
* @param newText 新文本
*/
function diffText(oldText?: string, newText?: string): DiffSegment[] {
if (!newText) return []
if (!oldText) return [{ text: newText, highlight: true }]
if (oldText === newText) return [{ text: newText, highlight: false }]
return computeDiff(oldText, newText)
}
/**
* 从旧数据列表中取出指定索引项的某个字段值
* @param oldList 旧数据列表
* @param itemIdx 项索引
* @param field 字段名
*/
function getOldFieldText(oldList: any[] | undefined, itemIdx: number, field: string): string {
if (!oldList || !oldList[itemIdx]) return ''
return oldList[itemIdx][field] || ''
}
/**
* 对比带description数组的经历列表中某一项描述文本的差异
* @param oldList 旧数据列表
* @param itemIdx 经历项索引
* @param descIdx 描述段落索引
* @param newText 新文本
*/
function diffListText(oldList: any[] | undefined, itemIdx: number, descIdx: number, newText: string, _type?: string): DiffSegment[] {
if (!oldList || !oldList[itemIdx] || !oldList[itemIdx].description || !oldList[itemIdx].description[descIdx]) {
return [{ text: newText, highlight: true }]
}
const oldText = oldList[itemIdx].description[descIdx].text || ''
if (oldText === newText) return [{ text: newText, highlight: false }]
return computeDiff(oldText, newText)
}
/**
* 对比教育经历描述文本的差异
* @param oldEducations 旧教育经历列表
* @param eduIdx 教育经历索引
* @param descIdx 描述段落索引
* @param newText 新文本
*/
function diffDescText(oldEducations: ResumeEducation[] | undefined, eduIdx: number, descIdx: number, newText: string): DiffSegment[] {
if (!oldEducations || !oldEducations[eduIdx] || !oldEducations[eduIdx].description || !oldEducations[eduIdx].description![descIdx]) {
return [{ text: newText, highlight: true }]
}
const oldText = oldEducations[eduIdx].description![descIdx].text || ''
if (oldText === newText) return [{ text: newText, highlight: false }]
return computeDiff(oldText, newText)
}
</script>