AI助手和Nova助手
This commit is contained in:
+45
-3
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
/** 默认 Token */
|
||||
const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
|
||||
/** 默认 Token(Nova 对话接口使用) */
|
||||
export const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
|
||||
|
||||
/**
|
||||
* AI 端专用 axios 实例
|
||||
@@ -17,7 +17,7 @@ const aiService = axios.create({
|
||||
* 浏览器安全策略不允许 JS 直接设置 Cookie header,改用 Token header 传递
|
||||
*/
|
||||
aiService.interceptors.request.use((config) => {
|
||||
config.headers['Token'] = DEFAULT_TOKEN
|
||||
// config.headers['Token'] = DEFAULT_TOKEN
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -71,3 +71,45 @@ export function uploadResume(file: File) {
|
||||
}
|
||||
|
||||
export default aiService
|
||||
|
||||
|
||||
// ==================== Nova 对话接口 ====================
|
||||
|
||||
/** Nova 对话历史消息项 */
|
||||
export interface NovaChatHistoryItem {
|
||||
/** 角色:user-用户 assistant-AI助手 */
|
||||
role: 'user' | 'assistant'
|
||||
/** 消息内容 */
|
||||
content: string
|
||||
}
|
||||
|
||||
/** Nova 对话请求参数 */
|
||||
export interface NovaChatParams {
|
||||
/** 用户输入的消息 */
|
||||
message: string
|
||||
/** 简历 ID */
|
||||
resumeId: number | string
|
||||
/** 当前浏览岗位 ID,不传则无岗位上下文 */
|
||||
jobId?: number | string
|
||||
/** 历史对话,前端维护 */
|
||||
history?: NovaChatHistoryItem[]
|
||||
}
|
||||
|
||||
/** Nova 对话返回数据 */
|
||||
export interface NovaChatResponse {
|
||||
/** AI 回复,Markdown 格式 */
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Nova AI 对话
|
||||
* POST /nova-chat/chat
|
||||
* @param params 对话参数
|
||||
*/
|
||||
export function sendNovaChat(params: NovaChatParams) {
|
||||
return aiService.post<NovaChatResponse>('/nova-chat/chat', params, {
|
||||
headers: {
|
||||
Token: DEFAULT_TOKEN,
|
||||
},
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import html2pdf from 'html2pdf.js'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import {
|
||||
fetchResumeMain, fetchResumeEducation, fetchResumeWork,
|
||||
fetchResumeInternship, fetchResumeProject, fetchResumeCompetition,
|
||||
} from '@/api/resume'
|
||||
|
||||
/**
|
||||
* 将简历DOM导出为PDF文件
|
||||
* @param element 简历DOM元素
|
||||
* @param fileName 文件名(不含扩展名)
|
||||
*/
|
||||
export async function exportResumePdf(element: HTMLElement, fileName: string) {
|
||||
const options = {
|
||||
margin: [10, 10, 10, 10] as [number, number, number, number],
|
||||
filename: `${fileName}.pdf`,
|
||||
image: { type: 'jpeg' as const, quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true, logging: false },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
|
||||
pagebreak: { mode: ['css', 'legacy'] },
|
||||
}
|
||||
await html2pdf().set(options).from(element).save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将简历DOM导出为Word文件
|
||||
* @param element 简历DOM元素
|
||||
* @param fileName 文件名(不含扩展名)
|
||||
*/
|
||||
export function exportResumeWord(element: HTMLElement, fileName: string) {
|
||||
// Word专用内联样式(不使用页面全局样式,避免 rem/100px 基准干扰)
|
||||
// 字体在第一版基础上放大50%,边距通过Word XML指令设置
|
||||
const wordCss = `
|
||||
@page WordSection1 {
|
||||
size: 595.3pt 841.9pt;
|
||||
mso-page-orientation: portrait;
|
||||
mso-header-margin: 0pt;
|
||||
mso-footer-margin: 0pt;
|
||||
mso-paper-source: 0;
|
||||
margin-top: 72pt;
|
||||
margin-right: 54pt;
|
||||
margin-bottom: 72pt;
|
||||
margin-left: 54pt;
|
||||
}
|
||||
div.WordSection1 { page: WordSection1; }
|
||||
body { font-family: 'SimSun', 'Songti SC', serif; color: #000; line-height: 1.6; margin: 0; padding: 0; }
|
||||
.job-resume-template { width: 100%; background: #fff; box-sizing: border-box; }
|
||||
.resume-html { padding: 0; font-family: 'SimSun', 'Songti SC', serif; color: #000; line-height: 1.6; }
|
||||
.resume-html__name { font-size: 24pt; font-weight: 700; margin: 0 0 4.8pt 0; line-height: 1.3; }
|
||||
.resume-html__contact { font-size: 12pt; color: #000; margin-bottom: 3.6pt; line-height: 1.5; }
|
||||
.resume-html__contact-row { display: flex; align-items: center; gap: 0; }
|
||||
.resume-html__separator { margin: 0 4.8pt; color: #777; }
|
||||
.resume-html__section-title { font-size: 17.3pt; font-weight: 700; color: #000; margin-top: 20pt; margin-bottom: 10pt; line-height: 1.3; }
|
||||
.resume-html__divider { height: 1.5px; background: #000; margin-bottom: 9.6pt; }
|
||||
.resume-html__summary { font-size: 12pt; line-height: 1.7; margin-bottom: 4.8pt; }
|
||||
.resume-html__item { margin-bottom: 9.6pt; }
|
||||
.resume-html__item-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 3.6pt; }
|
||||
.resume-html__item-left { display: flex; flex-direction: column; gap: 1.2pt; }
|
||||
.resume-html__item-main { font-size: 13.2pt; font-weight: 600; color: #000; line-height: 1.4; }
|
||||
.resume-html__item-right { display: flex; align-items: center; gap: 4.8pt; flex-shrink: 0; text-align: right; }
|
||||
.resume-html__item-location { font-size: 12pt; color: #000; }
|
||||
.resume-html__item-date { font-size: 12pt; color: #000; white-space: nowrap; }
|
||||
.resume-html__item-desc { font-size: 12pt; color: #000; line-height: 1.5; }
|
||||
.resume-html__desc-list { margin: 0; padding-left: 0; list-style: none; mso-list: none; }
|
||||
.resume-html__desc-list li { font-size: 12pt; line-height: 1.7; color: #000; list-style: none; mso-list: none; margin-left: 0; padding-left: 24pt; text-indent: 0; }
|
||||
.resume-html__skills { font-size: 12pt; line-height: 1.7; }
|
||||
.resume-html__skill-row { margin-bottom: 2.4pt; }
|
||||
.resume-html__skill-label { font-weight: 600; }
|
||||
.resume-html__diff-highlight { background-color: #D4EDDA; color: #155724; border-radius: 2px; padding: 0 1px; }
|
||||
`
|
||||
|
||||
// 组装Word可识别的HTML文档
|
||||
// Word页面边距通过XML指令设置(单位twips,1pt=20twips,1英寸=1440twips)
|
||||
// 上下边距 2cm ≈ 1134twips,左右边距 2.5cm ≈ 1418twips
|
||||
const parts: string[] = []
|
||||
parts.push('<html xmlns:o="urn:schemas-microsoft-com:office:office"')
|
||||
parts.push(' xmlns:w="urn:schemas-microsoft-com:office:word"')
|
||||
parts.push(' xmlns="http://www.w3.org/TR/REC-html40">')
|
||||
parts.push('<head>')
|
||||
parts.push('<meta charset="utf-8">')
|
||||
parts.push('<meta name="ProgId" content="Word.Document">')
|
||||
parts.push('<meta name="Generator" content="Microsoft Word 15">')
|
||||
parts.push('<!--[if gte mso 9]><xml>')
|
||||
parts.push('<w:WordDocument>')
|
||||
parts.push('<w:View>Print</w:View>')
|
||||
parts.push('<w:Zoom>100</w:Zoom>')
|
||||
parts.push('</w:WordDocument>')
|
||||
parts.push('</xml><![endif]-->')
|
||||
parts.push('<style>' + wordCss + '</style>')
|
||||
parts.push('</head>')
|
||||
parts.push('<body>')
|
||||
parts.push('<!--[if gte mso 9]><xml>')
|
||||
parts.push('<w:WordDocument>')
|
||||
parts.push('<w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel>')
|
||||
parts.push('</w:WordDocument>')
|
||||
parts.push('</xml><![endif]-->')
|
||||
parts.push('<div class="WordSection1">' + element.outerHTML + '</div>')
|
||||
parts.push('</body>')
|
||||
parts.push('</html>')
|
||||
const fullHtml = parts.join('\n')
|
||||
|
||||
// 生成Blob并触发下载
|
||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = `${fileName}.doc`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
}
|
||||
|
||||
/** 学历文字转数字 */
|
||||
function degreeToNumber(degree?: string): number {
|
||||
const map: Record<string, number> = { '大专': 1, '本科': 2, '硕士': 3, '博士': 4 }
|
||||
return map[degree || ''] || 2
|
||||
}
|
||||
|
||||
/** 描述段落映射辅助函数 */
|
||||
function mapDesc(list?: { id?: string; text?: string }[]) {
|
||||
return (list || []).map(d => ({ id: d.id, text: d.text || '' }))
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载简历完整数据并组装为模板格式
|
||||
* @param resumeId 简历ID
|
||||
*/
|
||||
export async function loadResumeTemplateData(resumeId: string): Promise<ResumeTemplateData | null> {
|
||||
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) return null
|
||||
|
||||
const r = mainRes.data
|
||||
return {
|
||||
name: r.name || '未填写姓名',
|
||||
email: r.email || '',
|
||||
mobileNumber: r.mobileNumber || '',
|
||||
wechatNumber: r.wechatNumber || '',
|
||||
summary: r.summary || '',
|
||||
educations: (eduRes.data || []).map(e => ({
|
||||
school: e.school || '',
|
||||
major: e.major || '',
|
||||
degree: degreeToNumber(e.degree),
|
||||
startDate: e.startDate || '',
|
||||
endDate: e.endDate || '',
|
||||
description: mapDesc(e.description),
|
||||
})),
|
||||
workExperiences: (workRes.data || []).map(w => ({
|
||||
companyName: w.companyName || '',
|
||||
position: w.position || '',
|
||||
startDate: w.startDate || '',
|
||||
endDate: w.endDate || '',
|
||||
description: mapDesc(w.description),
|
||||
})),
|
||||
internships: (internRes.data || []).map(i => ({
|
||||
companyName: i.companyName || '',
|
||||
position: i.position || '',
|
||||
startDate: i.startDate || '',
|
||||
endDate: i.endDate || '',
|
||||
description: mapDesc(i.description),
|
||||
})),
|
||||
projects: (projRes.data || []).map(p => ({
|
||||
projectName: p.projectName || '',
|
||||
companyName: p.companyName || '',
|
||||
role: p.role || '',
|
||||
startDate: p.startDate || '',
|
||||
endDate: p.endDate || '',
|
||||
description: mapDesc(p.description),
|
||||
})),
|
||||
competitions: (compRes.data || []).map(c => ({
|
||||
competitionName: c.competitionName || '',
|
||||
award: c.award || '',
|
||||
awardDate: c.awardDate || '',
|
||||
description: mapDesc(c.description),
|
||||
})),
|
||||
skills: r.skills || [],
|
||||
certificates: r.certificates || [],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user