简历上传,pdf简历下载
This commit is contained in:
@@ -304,6 +304,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import html2pdf from 'html2pdf.js'
|
||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import { fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
|
||||
@@ -588,10 +589,82 @@ function toggleDownloadMenu() {
|
||||
}
|
||||
|
||||
/** 处理下载(PDF/Word) */
|
||||
function handleDownload(type: 'pdf' | 'word') {
|
||||
async function handleDownload(type: 'pdf' | 'word') {
|
||||
showDownloadMenu.value = false
|
||||
// TODO: 实现简历HTML转PDF/Word下载
|
||||
console.log(`[下载简历] 格式: ${type}`)
|
||||
|
||||
if (type === 'pdf') {
|
||||
// 通过 JobResumeTemplate 组件暴露的 resumeRef 获取简历DOM
|
||||
const element = resumeTemplateRef.value?.resumeRef
|
||||
if (!element) {
|
||||
console.error('[下载简历] 无法获取简历模板DOM')
|
||||
return
|
||||
}
|
||||
|
||||
// html2pdf 配置选项
|
||||
const options = {
|
||||
margin: [10, 10, 10, 10] as [number, number, number, number],
|
||||
filename: `${resumeTemplateData.value.name || '简历'}_定制简历.pdf`,
|
||||
image: { type: 'jpeg', quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true, logging: false },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
|
||||
}
|
||||
|
||||
try {
|
||||
await html2pdf().set(options).from(element).save()
|
||||
} catch (err) {
|
||||
console.error('[下载简历] PDF生成失败', err)
|
||||
}
|
||||
} else {
|
||||
// 将简历HTML转为Word文档并下载(使用HTML格式的.doc文件,Word可正常打开)
|
||||
const element = resumeTemplateRef.value?.resumeRef
|
||||
if (!element) {
|
||||
console.error('[下载简历] 无法获取简历模板DOM')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取页面样式表内容
|
||||
const styleSheets = Array.from(document.styleSheets)
|
||||
let cssText = ''
|
||||
styleSheets.forEach((sheet) => {
|
||||
try {
|
||||
Array.from(sheet.cssRules).forEach((rule) => {
|
||||
cssText += rule.cssText + '\n'
|
||||
})
|
||||
} catch {
|
||||
// 跨域样式表无法读取,跳过
|
||||
}
|
||||
})
|
||||
|
||||
// 组装完整HTML文档(Word可识别的HTML格式)
|
||||
const fullHtml = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="ProgId" content="Word.Document">
|
||||
<meta name="Generator" content="Microsoft Word 15">
|
||||
<style>${cssText}</style>
|
||||
</head>
|
||||
<body>${element.outerHTML}</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 生成Blob并触发下载
|
||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||
const fileName = `${resumeTemplateData.value.name || '简历'}_定制简历.doc`
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (err) {
|
||||
console.error('[下载简历] Word生成失败', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 立即去投递 */
|
||||
|
||||
@@ -109,11 +109,9 @@
|
||||
<span class="profile-drawer__required">*</span>学历类型
|
||||
</label>
|
||||
<div class="profile-drawer__select-wrap">
|
||||
<select class="profile-drawer__input" v-model.number="edu.studyType">
|
||||
<option :value="0">全日制</option>
|
||||
<option :value="1">非全日制</option>
|
||||
<select class="profile-drawer__input" v-model="edu.studyType">
|
||||
<option v-for="opt in studyTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
<!-- <svg viewBox="0 0 16 16" fill="none" class="profile-drawer__select-arrow"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-drawer__field profile-drawer__field--half">
|
||||
@@ -121,13 +119,9 @@
|
||||
<span class="profile-drawer__required">*</span>学历
|
||||
</label>
|
||||
<div class="profile-drawer__select-wrap">
|
||||
<select class="profile-drawer__input" v-model.number="edu.degree">
|
||||
<option :value="1">大专</option>
|
||||
<option :value="2">本科</option>
|
||||
<option :value="3">硕士</option>
|
||||
<option :value="4">博士</option>
|
||||
<select class="profile-drawer__input" v-model="edu.degree">
|
||||
<option v-for="opt in degreeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
<!-- <svg viewBox="0 0 16 16" fill="none" class="profile-drawer__select-arrow"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -742,6 +736,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ========== 个人概述模块(仅简历使用) ========== -->
|
||||
<template v-else-if="module === 'summary'">
|
||||
<div class="profile-drawer__field">
|
||||
<label class="profile-drawer__label">个人概述</label>
|
||||
<textarea
|
||||
class="profile-drawer__textarea"
|
||||
placeholder="请输入个人概述,简要介绍自己的优势和职业目标"
|
||||
v-model="summaryText"
|
||||
rows="8"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ========== 其他模块占位 ========== -->
|
||||
<template v-else>
|
||||
<div class="profile-drawer__empty">
|
||||
@@ -770,10 +777,10 @@ interface EducationItem {
|
||||
school: string
|
||||
/** 专业 */
|
||||
major: string
|
||||
/** 学历类型(0=全日制 1=非全日制) */
|
||||
studyType: number
|
||||
/** 学历(1=大专 2=本科 3=硕士 4=博士) */
|
||||
degree: number
|
||||
/** 学历类型(数字模式:0=全日制 1=非全日制;文本模式:中文字符串) */
|
||||
studyType: number | string
|
||||
/** 学历(数字模式:1=大专 2=本科 3=硕士 4=博士;文本模式:中文字符串) */
|
||||
degree: number | string
|
||||
/** 入学时间(日期选择器使用字符串,格式:YYYY-MM) */
|
||||
startDate: string
|
||||
/** 毕业时间(日期选择器使用字符串,格式:YYYY-MM) */
|
||||
@@ -854,8 +861,22 @@ const props = defineProps<{
|
||||
module: string
|
||||
/** 初始数据 — 不同模块传入不同 JSON 结构 */
|
||||
initialData?: Record<string, any>
|
||||
/** 教育经历是否使用中文文本作为选项值(默认 false 使用数字编码,true 则输出中文字符串) */
|
||||
useEducationTextValue?: boolean
|
||||
}>()
|
||||
|
||||
/** 学历类型选项映射 — 根据 useEducationTextValue 决定 value 类型 */
|
||||
const studyTypeOptions = computed(() => props.useEducationTextValue
|
||||
? [{ value: '全日制', label: '全日制' }, { value: '非全日制', label: '非全日制' }]
|
||||
: [{ value: 0, label: '全日制' }, { value: 1, label: '非全日制' }]
|
||||
)
|
||||
|
||||
/** 学历选项映射 — 根据 useEducationTextValue 决定 value 类型 */
|
||||
const degreeOptions = computed(() => props.useEducationTextValue
|
||||
? [{ value: '大专', label: '大专' }, { value: '本科', label: '本科' }, { value: '硕士', label: '硕士' }, { value: '博士', label: '博士' }]
|
||||
: [{ value: 1, label: '大专' }, { value: 2, label: '本科' }, { value: 3, label: '硕士' }, { value: 4, label: '博士' }]
|
||||
)
|
||||
|
||||
/** 组件 Emits */
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
@@ -868,6 +889,7 @@ const store = useStore()
|
||||
/** 模块名称映射表 — 模块标识 → 中文标题 */
|
||||
const moduleTitleMap: Record<string, string> = {
|
||||
info: '基本信息',
|
||||
summary: '个人概述',
|
||||
education: '教育经历',
|
||||
work: '工作经历',
|
||||
internship: '实习经历',
|
||||
@@ -943,8 +965,8 @@ const educationList = ref<EducationItem[]>([])
|
||||
const createEmptyEducation = (): EducationItem => ({
|
||||
school: '',
|
||||
major: '',
|
||||
studyType: 0,
|
||||
degree: 2,
|
||||
studyType: props.useEducationTextValue ? '全日制' : 0,
|
||||
degree: props.useEducationTextValue ? '本科' : 2,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
description: [{ id: generateId(), text: '' }],
|
||||
@@ -1099,6 +1121,9 @@ const skillsList = ref<string[]>([])
|
||||
/** 作品集链接 — 作品集模块使用 */
|
||||
const portfolioUrl = ref('')
|
||||
|
||||
/** 个人概述文本 — 个人概述模块使用 */
|
||||
const summaryText = ref('')
|
||||
|
||||
/** 新技能输入框的值 */
|
||||
const newSkillInput = ref('')
|
||||
|
||||
@@ -1205,6 +1230,9 @@ watch(() => props.modelValue, (visible) => {
|
||||
} else if (props.module === 'portfolio') {
|
||||
// 作品集:用初始数据填充链接
|
||||
portfolioUrl.value = props.initialData?.portfolioUrl || ''
|
||||
} else if (props.module === 'summary') {
|
||||
// 个人概述:用初始数据填充文本
|
||||
summaryText.value = props.initialData?.summary || ''
|
||||
} else if (props.module === 'skills') {
|
||||
// 技能:用初始数据填充列表,若无则创建空数组
|
||||
skillsList.value = props.initialData?.skills ? [...props.initialData.skills] : []
|
||||
@@ -1256,6 +1284,8 @@ const handleSave = () => {
|
||||
})) })
|
||||
} else if (props.module === 'portfolio') {
|
||||
emit('save', { portfolioUrl: portfolioUrl.value })
|
||||
} else if (props.module === 'summary') {
|
||||
emit('save', { summary: summaryText.value })
|
||||
} else if (props.module === 'skills') {
|
||||
emit('save', { skills: [...skillsList.value] })
|
||||
} else if (props.module === 'certificate') {
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<div class="side-nav">
|
||||
<!-- 顶部 Logo -->
|
||||
<div class="side-nav__header">
|
||||
<div class="side-nav__avatar">
|
||||
<span class="side-nav__avatar-icon">👤</span>
|
||||
</div>
|
||||
<span class="side-nav__logo-text">Offer派</span>
|
||||
|
||||
<!-- 侧边栏Logo图片 -->
|
||||
<img src="@/assets/images/logo.png" alt="Offer派" class="side-nav__logo-img" />
|
||||
</div>
|
||||
|
||||
<!-- 主导航 -->
|
||||
|
||||
Reference in New Issue
Block a user